简介:《Ascend C算子开发能力认证(初级)题库》是面向华为Ascend AI芯片平台初级认证考生的备考资料,也适合对AI算子开发感兴趣的初学者自学使用,亦可用作高校人工智能相关课程的课后练习。文档以单个PDF文件提供,压缩包大小约155KB,内容精炼,可快速浏览或打印复习;PDF文档结构清晰,适合按章节顺序学习或选择性刷题。题库按章节组织,包含判断题、单选题、多选题等丰富题型,覆盖核函数定义与声明、矢量编程范式中的CopyIn/CopyOut/Compute流程、Queue队列内存管理、GlobalTensor与LocalTensor的使用,以及CPU侧调试、NPU侧运行验证等高频考点;还特别涉及核函数声明语法、Queue内存初始化、VECIN/VECOUT数据搬入搬出等易错细节。同时配有模拟测试题,方便考生自测学习效果,并针对薄弱环节进行强化复习。目前已有3478人学习下载,适合想要系统备战Ascend C算子初级认证,或希望巩固C算子开发实战能力的技术人员。
1. 这份 Ascend C 题库值得认真对待:它考的不是背题,而是上板思路
当你拿到 Ascend C 算子开发能力认证(初级)题库.pdf,最常见的动作是把选择题过一遍,然后去找“标准答案”。但真正到了认证现场,拉开分差的从来不是某道题选 A 还是选 C,而是你看到一个算子需求之后,能不能把“数据搬进来、算完、写回去”这一套流程在十几分钟内写完整。题库只是把考点列出来,消化它需要的是从 CPU 思维到 AI 处理器内存模型的转换。适合谁?想做算子开发却一直没找到入口的同学,以及写过一点 Python 但没碰过异构编程的同学,它能给你一条最短的验收路径。
2. 初级题库在考什么:把“算子”放到 AI Core 的四级分工里看
2.1 一次算子调用,背后其实是两级代码
先定义算子。算子是神经网络里的最小计算单元:一个加法、一个卷积、一个归一化,都对应一个算子。在 CPU 上写一个函数就结束了,在 AI 处理器上不行,因为它不是“一个统一内存的大核”,而是由控制核和大量计算核组成的异构系统。控制核运行的是 Host 侧代码,负责准备输入、分配输出、下发任务;计算核运行的是 Device 侧代码,也就是 Kernel 函数,由 AI Core 执行。
初级认证的题目大多数围绕 Device 侧展开,但选择题里偶尔会出现“以下哪段逻辑应该放在 Host 侧”这类题。如果你只是背“Tiling 计算在 Host 侧”这句话,换个问法就懵;如果你自己画过一遍两级代码的分工,看到这类题基本不用想。Host 侧解决“怎么安排任务”,Device 侧解决“每个任务怎么算”,这个分界在题库里反复出现。
还有一个容易忽略的点:算子的输入输出由框架以全局地址传进来,Kernel 函数拿到的是一串地址,而不是现成的数组。你在 CPU 上可以随便解引用一个指针,但在 AI 处理器上,这串地址指向的是片外存储,计算单元不能直接访问。绝大多数初次接触的人,第一次写算子都会在这里栽跟头,以为拿到地址就能像 C 语言一样逐元素操作。
2.2 从“数据元素”到“数据搬运”:存储层次是又一个背诵重灾区
AI Core 不能直接访问片外主存。数据要先从全局内存搬到片上局部内存,计算单元只认局部缓冲里的数据。这个缓冲在不同资料里有不同叫法,最常见的是 Unified Buffer,简称 UB。初级题库里对这一块的考察方式非常直接:数据搬移要经过哪个存储级别、搬移的单位是字节还是元素、搬入和搬出的方向怎么判断。
很多同学刷题时只记接口名,却忽略了接口背后的语义。搬移单位这一点特别要命:有的接口按字节数填,有的按元素个数填,填错之后编译可能不报错,但 UB 容量立刻算错。数据在半路被截断,输出结果就是一团乱码。你在看题库解析时,不要只记住“这道题选 B”,要在旁边写一句“按字节算,FP32 每个元素占 4 字节”,下次换成 FP16 才不会掉进同一个坑。
存储层次的另一个考法是“谁在搬、谁在算”。向量计算单元只能读局部缓冲里的数据,矩阵计算单元也有自己对应的片上缓冲。题目如果把“向量算子直接读全局内存”设为正确选项,那基本是送分题;但放到代码阅读题里,它藏在某个看起来很正常的循环里,就很容易被放过去。所以刷题时给自己一个条件反射:看到一次计算,先问数据现在在哪,再问它要去哪。
2.3 Tiling 才是初级认证里的隐形主线
为什么必须有 Tiling?因为片上内存装不下整张张量。全局内存可能有几十 MB,UB 只有几十到几百 KB,一次全搬进去不可能。做法是分块:每次搬一个 tile 到 UB,算完写回,再搬下一个。这个“切块”的过程就是 Tiling。
题库里大量计算题都在围着 Tiling 转:数据总量多大、每个 tile 多少元素、一共多少个 tile、最后一个 tile 剩多少、多核情况下每个核分到哪一段。这些计算本身不复杂,小学除法而已。但它有两个容易翻车的点:一是单位混用,题里给的容量是 KB,数据类型占的字节数是 2 或 4,一除就错;二是余数处理,最后一段不够一个完整 tile 时,边界用 min 截断还是直接放行,决定了程序会不会越界。
我建议你把 Tiling 相关的题目单独整理成一个分类,而不是混在“算子 API”里。因为它横跨多个题型:选择题考概念,计算题考参数推导,改错题考边界条件,编程题考落地写法。把这个主线拉通了,题库里至少三成题目会变得重复,因为它们换着花样在考同一件事:数据怎么切、切完怎么分、分完怎么不越界。
2.4 一个算子的最小执行流程:六步闭环
把上面这些内容压缩成一张表,就是一个算子从启动到结束的六步流程。题库里的代码阅读题和补全题,基本都在考这六步里的某一步。
| 步骤 | 常见写法 | 容易考到的点 |
|---|---|---|
| 1. 解析全局入参 | 从 GM_ADDR 读取输入输出地址 | 地址不能直接当数组解引用 |
| 2. 绑定全局内存 | 设置 GlobalTensor 的 buffer 和长度 | 长度按元素数还是字节数要核对 |
| 3. 计算 Tiling 参数 | 算出每核每 tile 处理多少元素 | 字节数先统一,再换算元素个数 |
| 4. 按当前核切分任务 | 获取 block_idx,计算本核区间 | 区间要用 [start, end) 且 min 截断 |
| 5. 搬入、计算、搬出 | 先拷到 UB,算完再拷回全球内存 | 三步顺序反了,结果是全零 |
| 6. 结束与同步 | 完成写回,返回 | 多核并发时要等所有核完成 |
这六步比背二十个接口名更有用。你做题时遇到任何一段 Kernel 代码,先试着把它映射到这张表上;如果一段代码能对应上,多半没问题;如果对应不上,比如出现了“先计算再搬入”,那这道题大概率在考顺序错误。我把这张表贴在显示器旁边刷了两天题,代码阅读题的正确率明显比硬背接口高。
3. 刷题不要背题面:把高频考点转化成“代入参数就能算”的推导套路
3.1 先把题分成两类:要动笔算的,和不要动笔算的
题库里那些“以下哪项是正确描述”的选择题,看似需要背,其实画一遍数据流图就能判断。比如问你“计算单元从哪里读取输入”,你把“全局内存 → 局部缓冲 → 计算单元 → 写回”这张图画在草稿纸上,每个选项对照一遍,错选项往往是把中间某条边接反了。这一类题不需要记原文,需要的是流程反射。
另一类题必须动笔算。比如“输入形状为 (512, 1024) 的 FP32 数据,UB 容量为 X KB,一次最多能搬多少行”。这种题如果靠记忆以前做过的相似题,考场上只要换一个形状、换一个数据类型,你立刻不会。我的做法是建一个固定推导模板,每次遇到容量题都按同一个顺序算:容量 → 可用字节 → 单元素字节数 → 单行字节数 → 一次几行。
这样做的好处是,你不需要每次重新想逻辑,只要往里代数字。模板一旦固定,做题速度会快很多,而且不容易漏掉单位换算。题库里这类计算题的变体非常多,但核心都是这个套路。
3.2 容量计算的最小公式:字节先统一,再谈元素
我在本地用一个 Python 小函数来复算题库里的容量题,避免自己心算出错:
def calc_tile_count(tile_bytes: int, dtype_size: int, reserve_bytes: int = 32): # 去掉一些固定开销后,真正能装数据的字节数 usable = max(0, tile_bytes - reserve_bytes) # 按元素个数返回,注意是整除,有余数要单独处理 return usable // dtype_size # 举例:UB 给 256KB,FP32 占 4 字节 ub_bytes = 256 * 1024 elements = calc_tile_count(ub_bytes, 4) print(elements) # 65536 个元素,这是理论上限这个函数的逻辑说明很简单:先扣掉队列头等固定开销,再用剩余字节数除以单个元素的字节数,得到最多能放多少个元素。参数里tile_bytes是考题给的 UB 容量,dtype_size随数据类型变化,FP16 是 2,FP32 和 INT32 是 4,INT8 是 1,reserve_bytes是留给管理结构的余量,考题通常会给,真实项目里要按文档确认。
注意题目问“一次能搬几行”时,别停在这个结果上。假设每一行有 1024 个 FP32,65536 除以 1024 就是 64 行。如果输入有两个数组要同时留在 UB 里做加法,还要把这个结果再除以 2。这类嵌套换算在题库里很常见,最容易漏的就是最后一步“同时存在几个数组”。
3.3 代码阅读题三个抓手:搬运方向、循环边界、多核索引
代码阅读题是初级题库里最像“实战”的题型。它不问你概念,而是给你一段代码,问你能不能发现问题。我总结出三个必查的抓手。
第一个是搬运方向。输入数据必须从全局内存搬到片上,计算完再从片上写回。代码里如果看到输出张量先被搬进 UB、输入张量直接参与计算,或者根本没写搬出,结果必然错误。这类错误在纸面上很好隐藏,因为它不报编译错误。
第二个是循环边界。越界是最隐蔽的问题。写for (i = 0; i <= total; i++)还是i < total,差别就一个等号,但真机上可能让最后一个核多读一个不存在的元素,导致随机崩溃。题库里改错题很喜欢在这里埋雷,因为大多数人读代码时关注逻辑,不关注边界。
第三个是多核索引。每个核通过 block_idx 拿到自己的编号,然后根据编号计算自己处理哪一段数据。如果所有核用同一个起始位置和同一个长度,最后结果是所有核写同一块输出,互相覆盖。正确写法是start = block_idx * per_block,end = min(start + per_block, total)。看到代码里出现block_idx,就立刻检查 start 和 end 是不是依赖它。
3.4 算子类型对应计算单元:选错单元等于整题做错
初级题库里有一类送分题,但错误率不低:给一个算子名,问它在哪个计算单元上执行。逐元素算子,比如 Add、Muls、激活函数,走向量计算单元;矩阵乘法,走矩阵计算单元;标量运算,比如地址计算、循环计数,走标量单元。规约类算子比如 Sum、Mean,主体在向量单元完成,但跨核汇总时要经过全局内存。
我见过不少人把“逐元素算子用矩阵单元执行”当正确选项选上,理由是“矩阵单元算得快”。这就是没有建立对应关系。你在刷题时每遇到一个算子,在题旁边写一行“这个算子的核心操作是什么”,加法是逐元素,卷积是乘加,矩阵乘是乘加,看起来相似,但实现路径完全不同。考试不会故意刁难你,它考的就是这套基本映射。
3.5 别停在“看懂答案”:把题改成“删一行再补”的模式
刷题到后期,容易被一种假象骗住:选择题正确率很高,以为自己掌握了。这时候我建议换一种方式重做旧题——把每道代码题的标准答案遮住,然后自己写一遍;或者故意删掉其中关键一行,比如删掉多核索引计算,再让身边的人看看能不能发现问题。
以多核写入为例,删掉start = block_idx * per_block这一行后,所有核都会从 0 开始写前一段数据。代码能编译、能运行,但输出数组里只有最后完成那个核的结果,其他核写的都被覆盖。这种“能跑但错得离谱”的题,最接近真实上机时的翻车现场。你可以准备一个小本子,专门记录“删掉哪个点会造成什么现象”,考前翻一遍,比反复看选择题解析管用得多。
4. 不上板也能验证算子逻辑:用 NumPy 搭一个 Tiling 语义模拟器
4.1 为什么值得搭一个本地模拟器
刷题刷到计算题阶段,你会遇到一个尴尬:自己算出来的 tile 切分和边界条件,到底对不对?题库答案只给数字,不给你“为什么是这个数”。如果每一道都靠心算核对,不仅慢,而且错的时候不知道错在哪一步。我习惯把 Tiling 相关的推导写成 NumPy 模拟器跑一遍,用几十行代码把抽象的容量题变成可验证的输入输出关系。
这个模拟器的目标不是复现 Ascend C 的真实算子行为,而是复现“切块、分核、边界截断”这些逻辑。你把题里的数字代进去,它返回每个核负责的区间;再把区间代进一个简单的加法函数,看输出是否和 NumPy 的原始结果一致。一致说明你的推导没错;不一致,就说明某个边界条件算错了,这时候回头看是哪一步,比死磕一页 PDF 有效得多。
4.2 最小切块函数:把一维数据切成若干个 tile
先写最基础的切块函数。它模拟的是“大张量按 tile_size 切成多段,每一段独立搬入 UB 处理”的过程:
import numpy as np def split_tiles(total: int, tile_size: int): """把 total 个元素切成若干个 tile,返回每段的 [start, end) 区间""" intervals = [] start = 0 while start < total: end = min(start + tile_size, total) intervals.append((start, end)) start = end return intervals tiles = split_tiles(1000, 256) print(tiles[:3], "……共", len(tiles), "个 tile")逻辑说明写在代码注释里了。start从 0 开始,每次前进tile_size;end取start + tile_size和total的最小值,这样最后一段不满一个 tile 时不会越界。split_tiles返回的是左闭右开区间,也就是包含start,不包含end,和你在上板代码里应该写的循环边界保持一致。
参数有两个:total是输入张量的元素总数,tile_size是每个 tile 的元素个数。你可能会问:为什么模拟器直接用元素个数,而真机用字节数?因为模拟器不需要考虑存储容量,它只验证区间推导;但你在做题时,tile_size必须先用“UB 容量 ÷ 单元素字节数”算出来,再把算出的值传给这个函数。单位和前面的容量计算是一脉相承的。
4.3 单核分块加法的模拟:把六步流程跑一遍
有了切块函数,就可以模拟一个最简单的单核加法算子。真机上的逻辑是“搬一块、算一块、写回一块”,模拟器用 NumPy 的切片赋值来模拟这个行为:
def vector_add_sim(x, y, tile_size): out = np.empty_like(x) n = x.shape[0] for start, end in split_tiles(n, tile_size): # 这一段等价于“搬入 UB -> 向量加法 -> 写回全局内存” out[start:end] = x[start:end] + y[start:end] return out x = np.arange(1000, dtype=np.float32) y = np.ones(1000, dtype=np.float32) * 2 result = vector_add_sim(x, y, tile_size=256) print(np.array_equal(result, x + y)) # True,证明切分逻辑没破坏结果这段代码的out[start:end] = x[start:end] + y[start:end]是核心:它把一次完整搬运加计算塞进一个切片里,和真机上“搬一小段 → 算一小段 → 写回一小段”是同一套节奏。tile_size=256表示每个 tile 处理 256 个元素;这里设为多少模型都能跑对,但你可以故意把它改小,比如改成 7,观察循环次数变多、最后一个 tile 只剩少量元素的情况。这正是真机上边界条件最容易出问题的场景。
你还可以用这个模拟器验证一个常见题目陷阱:如果写循环时把end写成了min(start + tile_size, n - 1),最后一个元素就会被丢掉。把代码里n改成n - 1跑一次,你会看到np.array_equal返回 False。这种“亲手跑出一个错误”的过程,比背十遍注意事项都深刻。
4.4 多核任务的分配模拟:BlockId 的语义就这么回事
单核模拟跑通之后,加上多核分配逻辑。每个核拿到自己的编号,然后从总任务里分一段,这就是block_idx在真机上的作用:
def assign_blocks(total: int, core_num: int): """把 total 个元素尽量均匀地分给 core_num 个核""" base = total // core_num remainder = total % core_num regions = [] offset = 0 for core_id in range(core_num): span = base + (1 if core_id < remainder else 0) regions.append((offset, offset + span)) offset += span return regions regions = assign_blocks(1000, 8) print(regions) # 前 4 个核拿 125 个元素,后 4 个核拿 124 个,总共正好 1000参数说明:total是元素总数,core_num是启动的计算核数。base = total // core_num是整除得到的每核基础份数,remainder是除不尽的余数。循环里让前remainder个核每人多拿 1 个元素,这样所有核的任务量只差 1,且总数正好分完。真机上你不需要手动写这个循环,但必须看得懂这个分配逻辑,因为题库里经常给出两个核的区间让你判断是否越界。
这里还有一个现实问题:如果core_num大于total,用这个函数算出来某些核会拿到空区间。空区间本身不致命,但如果你在代码里直接用空区间去搬数据,就会出错。所以严谨的写法要在循环里加判断:if span <= 0: continue。这个边界点也是初级题库里喜欢拿来出改错题的地方。
4.5 用 axis 验证规约题:同一份数据的两种换行方式
规约类算子是题库里的另一个高频考点,尤其是 Sum 和 Mean。模拟器能帮你搞清楚 axis 语义,因为 NumPy 的sum和 Ascend C 的规约在数学上是一致的:
x = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.float32) print(x.sum(axis=0)) # 按列求和,得到 [5, 7, 9] print(x.sum(axis=1)) # 按行求和,得到 [ 6, 15]这段代码本身就说明了两件事。axis=0是跨行操作,把两行的同一列相加;axis=1是行内操作,把一行的三个元素相加。在 AI 处理器上,axis=1方向通常是内存连续的,可以高效地一次搬一整行;axis=0方向跨行,需要多次搬移或者做转置,代价更高。题库里如果问“哪个 axis 更容易实现高效搬运”,想一想内存布局就能得出答案。
axis的另一个坑是“形状对但数据不对”。你写一个规约算子,如果轴的编号差一个,结果的形状可能恰好一样,但数值完全不同。模拟器能帮你快速验证:把x.sum(axis=0)和x.sum(axis=1)打印出来对比,你会发现两行结果的形状都是 1 维,但值完全不同。“看起来对、实际错”是这类题最阴的地方,建议你在刷题时养成一个习惯:任何规约题都先用 NumPy 跑一个 2×3 的小例子,把期望输出写在草稿上,再去做代码推导。
5. 刷题避坑:初级题库里最容易翻车的 5 个细节
5.1 坑一:把“搬运”当背景板,输出全是 0
现象:A 同学按模板写完一个加法算子,上板跑出来输出全是 0,只有第一个值对了。查代码,编译没报错,逻辑看着也没问题。原因:全局内存的数据没有搬入片上缓冲,直接在缓冲上做加法;缓冲初始值是 0,所以“0 + 0”永远等于 0。解决:把“搬入 → 计算 → 搬出”三步顺序写在代码注释里,每写一步就核对一次操作对象是全局张量还是片上缓冲。
这个坑在题库里通常以“以下哪段代码能正确实现加法”的形式出现。错误选项往往把计算放在搬入之前,或者干脆不写搬出。你只靠看代码可能觉得没问题,因为接口名都对了;但只要把数据流图在脑子里过一遍,立刻会发现顺序反了。
5.2 坑二:用元素个数算 UB 容量,结果溢出
现象:一道题给 UB 容量为 256KB,输入是 FP32 数组,你算出一次能放 65536 个元素,直接把 65536 当作 tile_size 写进代码。上板后程序报资源不足,或者数据被截断。原因:256KB 是字节数,FP32 单个元素占 4 字节,所以能放的元素数是 256×1024÷4 = 65536,这个除法你已经做了;但如果你把题里的“256”直接当成 KB 以外的某个数,或者跳过除以 4 这一步,就会把 tile_size 算大 4 倍。解决:所有容量题统一以字节为单位先算一遍,再除以单元素字节数。
还有一种变体是 FP16。FP16 每个元素 2 字节,同样 256KB 能放 131072 个元素,是 FP32 的两倍。题库很喜欢在同一道题里把数据类型换来换去,让你验证“按字节算”这个习惯。你在草稿上演算时,把“dtype_size”单独写一行,不要混在容量里心算。
5.3 坑三:多核任务边界写成左闭右闭
现象:多核程序偶发崩溃,每次崩的位置还不一样。判断题,可能是因为某个核处理的数据范围比它实际负责的范围多了一个元素。原因:循环边界写成了i <= end,也就是把右边界也包含了;当 end 恰好等于 total 时,最后一次访问越界。解决:全用左闭右开区间[start, end),写循环时用i < end。
在题库里,这个坑常被包装成“下面哪个区间写法是正确的”。四个选项分别给出(a, b]、[a, b)、(a, b)、[a, b]之类的区间范围,如果你没有建立“真机上边界越界是致命问题”的意识,很容易选错。我自己的习惯是做题时把区间端点代入最简单的例子,比如 total=10,核数=3,直接把每个核的 start 和 end 算出来,看是否覆盖 0 到 9 且互不重叠。
5.4 坑四:axis 和内存连续方向对不上
现象:写一个规约算子,axis 指定为 0,结果和 NumPy 的sum(axis=0)对比,数值完全对不上,但输出形状是对的。原因:你按 axis 的字面意思去实现,没有考虑内存里相邻元素到底是哪个方向。以形状为 (N, C) 的二维数据为例,内存中排布是一行一行连续存储的,同一行的相邻元素物理上相邻,跨行的同一列物理上相距一行。axis=1 可以直接顺序读,axis=0 需要跨 stride 访问。解决:拿到任何 axis 题,先把输入按内存布局展开成一维,看相邻两个元素在原始形状里是哪个维度,再决定搬运策略。
题库里如果出“以下哪种规约实现效率更高”,答案基本是“沿连续维度规约”。这不是玄学,是硬件决定的。模拟器部分提到用 NumPy 打印小例子对比,就是为了在考前把这些轴的物理含义印在脑子里。
5.5 坑五:只看 PDF 不写代码,上机手生
现象:选择题正确率有八成,但到了补全代码环节,卡在“下一行填什么”上,思考时间特别长,最后写出了错误的接口参数。原因:PDF 只给了结论,没给键盘上的反射弧。看代码和写代码是两种能力,刷题时眼睛先看到了标准写法,大脑会产生“我会了”的错觉。解决:每周至少三次,每次十几分钟,默写一个空算子模板的六步流程,不查资料,写完再和笔记对照。
这个坑不只在初级认证,所有“题库+Pdf”形式的备考都会遇到。它不算技术难点,但实际淘汰率很高。我见过一些人题刷得很熟,上机时连“全局地址怎么绑定到 Tensor 对象”都要想半天。把六步流程写到形成肌肉记忆,比多做二十道选择题有用。
6. 考前两周冲刺:把题库变成一张可以随身带的参数表
6.1 两周计划表:从读懂到默写
如果你距离认证还有两周,我给一个按天拆分的安排,每天投入一到两小时。重点不是刷题量,而是把题库内容拆成可复现的技术动作。
| 天数 | 安排 | 验证产出 |
|---|---|---|
| 第 1-2 天 | 把六步流程抄三遍,画数据流图 | 不看资料能画出完整流向 |
| 第 3-5 天 | 用 NumPy 模拟器跑题里所有 Tiling 计算 | 每个核的区间都能说出依据 |
| 第 6-8 天 | 每天两道容量计算题,只写三行推导 | 字节、元素、结果三个数全对 |
| 第 9-11 天 | 把代码阅读题改成改错题,删行后自查 | 说出删掉哪行会造成什么现象 |
| 第 12-14 天 | 全流程限时模拟,一题控制在二十分钟 | 六步流程一次写对,不查资料 |
6.2 一个小技巧:把换算关系做成一页纸
考前最后一天,不要再看密密麻麻的笔记,只做一张卡片。正面写数据类型占用字节数:FP16 是 2,FP32 和 INT32 是 4,INT8 是 1;背面写区间公式:start = block_idx * per_block,end = min(start + per_block, total),再写一个 2×3 的 axis 求和小例子。这张纸的作用不是给你考场上抄,而是让你在地铁上、排队时反复默想。它的价值在于把你所有需要“动笔算”的考点压缩进短时记忆,形成条件反射。
我当年第一次上机前只看 PDF,到了补全代码环节连“数据搬入该放在哪一步”都要犹豫,结果自然不理想。第二次老老实实做了两周模拟,每天都把那六步流程默写一遍,最终上机比想象中顺利得多。题库只是地图,真正让你过关的是把每个参数、每条边界、每个方向都内化成自己的判断。希望帮到你。
本文还有配套的精品资源,点击获取