系列目录 · 资源与调度 · Read in English
“刚才这个测试还过,单独跑也过,放到整套测试后面就变了。”
“是不是随机种子?”
“没有随机数。只有一个静态计数器。”
有时编译器的不确定性不来自复杂并行算法,而来自一个朴素到容易被忽略的全局状态:为循环标签编号的计数器。第一个测试用掉一些编号,第二个测试就从更大的数字开始;换个执行顺序,汇编文本又不同。
标签应该属于谁
标签用于在一个生成单元内标识控制流目标。因此自然的生命周期也是这个生成单元:在同一个指令流里不能重复,不同独立指令流里没有必要共享编号空间。
已核实的修改把若干内核内部的静态循环计数器,改为由生成器对象维护的标签分配接口。算术广播循环、复合循环和循环状态内核都从同一生成器取得标签。
这带来两个可以分开的性质。其一,在一个指令流里连续生成多个内核时,标签不会因为各自局部命名方式而碰撞。其二,两个全新的生成器处理相同输入时,标签编号不再取决于进程先前生成过多少程序。
它还移除了这些静态计数器在并行调用中的共享修改点。不过,这不等于生成器对象本身可被多个线程同时使用。新增测试让线程分别创建独立生成器,验证的是独立对象之间互不污染。
循环正确性有四层
第一层是计数正确。次数为 1 时应执行一次,不应意外额外推进一次地址。次数为 0 时,是明确不支持,还是生成空循环,必须由接口定义。若使用“先执行、再递减”的结构,把 0 当合法值可能产生巨大回绕循环。
第二层是地址正确。内层每轮前进一个步长,外层结束时可能需要另一个边界修正。只检查循环次数,无法发现下一行仍从上一行末尾继续的问题。
第三层是控制流正确。分支落点、回跳距离和伪指令展开后的指令长度必须一致。文本里写着正确标签,不代表最终编码的位移必然正确。
第四层是资源正确。嵌套计数器的寄存器不能互相覆盖,内部事件退出后要释放,连续生成多个内核时不能残留活跃状态。
这四层彼此关联,却不能由一句“循环测试通过”代替。
用一个教学二维循环对齐概念
考虑处理一个 3×5 的教学网格:
1 | outer = 3 |
这里故意把最后一轮的地址推进放在退出判断后面,因为有些描述符模型不允许在结束后再推进到越界地址。实际实现也可能采取别的合法方式,但测试必须对应其地址协议。
如果外层计数器在内层初始化时被重用,同一个小例子就可能永远到不了 all_done。这说明 寄存器与 RAII里的资源租约与循环结构必须一起设计。
黄金测试能够提供什么证据
相关提交增加了一组固定的二进制参考,覆盖无循环、单次、内层循环、嵌套循环、复合算子、多步循环状态以及部分模式组合。测试比较真实汇编、伪汇编和二进制,还检查标签唯一、增量输出与完整输出一致、重复读取增量输出已经耗尽,以及依赖资源没有残留。
同一批用例还反向执行一次,并在多个线程的独立生成器中重复构造。这样测试的重点从“某一次生成正确”扩展到“结果不依赖其他用例先做了什么”。
这是非常有价值的工程覆盖,但它仍然有边界。二进制黄金值可能把某个既有错误也固定下来;全量字节相等能捕获变化,却不能自动解释变化为何正确或错误。
因此在评审黄金文件更新时,不能只看“新输出已重新生成”。需要说明是寄存器选择变了、标签作用域变了、控制流位移修正了,还是算子语义真的变化了。参考数据应服务于解释,不能替代解释。
稳定文本和稳定机器码不是一回事
不同标签名字最终可能解析成相同分支位移,因此汇编文本不稳定时,机器码仍可能相同。反过来,文本标签看起来稳定,伪指令展开数量变化也可能让机器码跳转目标改变。
这就是同时检查三种表示的价值:伪汇编保留高层意图,真实汇编反映展开,二进制确认字段编码。哪个层级首先发生变化,常常能缩小问题范围。
增量输出还有独立的状态问题。若一个阶段读取了新增汇编,另一个阶段又读取新增二进制,各自游标应遵守自己的协议。比较完整与增量输出,并确认“第二次读取为空”,是在验证消费行为,而不只是验证生成内容。
循环参数验证也有开关语义
改动还为启用广播循环的配置检查正次数和字段可表示范围,同时保留未启用循环时对无效循环计数字段的忽略行为。
这不矛盾。配置对象里存在某个字段,不意味着所有模式都使用它。如果普通非循环路径完全不读取 loop_count,强制要求它为正可能破坏已有调用者;而循环模式一旦启用,就必须在写出指令之前拒绝非法次数。
需要注意,针对若干广播入口的检查不自动覆盖所有复合或循环内核。每个入口仍应有自己的参数合同,不能因为公共检查函数存在,就认定所有路径已受保护。
黄金文件更新前,先做一次人工轨迹检查
面对一大串变化的机器字,最容易发生的事是整体替换参考输出。更有解释力的流程,是先为一个小循环列出轨迹:每次到达循环头时计数器是多少,执行了第几个块,地址指向哪里,退出时一共处理了多少块。
对于教学的两层循环,可以先验证第一块、内层最后一块、外层切换后的第一块和全局最后一块。这四个点能快速揭示大多数步长重置和退出条件错误。随后再让解释器枚举完整轨迹,避免人工检查遗漏中间变化。
如果仅寄存器编号改变,轨迹应保持相同;如果指令展开多了一条,标签名字可以不变,但分支距离必须重新计算;如果去掉一次地址更新,需要说明它原本是否发生在最后有效访问之后。把差异分类以后,黄金文件更新才有可审查的理由。
并行确定性也应采用同样思路。独立任务生成相同输入时结果一致,是对象隔离的证据;多个任务共享一个对象时会怎样,属于另外的接口合同。不能因为线程测试出现过,就把任何使用方式都宣布线程安全。测试代码本身怎样构造对象,往往比测试名字更能说明它证明了什么。
回归与实验
| 维度 | 测试设计 | 能揭示的问题 |
|---|---|---|
| 次数 | 无循环、一次、多次、非法零值 | 回绕与多执行一次 |
| 嵌套 | 内外层分别取不同小整数 | 计数器覆盖、边界步长错误 |
| 组合 | 同一生成器连续发射不同内核 | 标签碰撞和残留资源 |
| 顺序 | 正序、反序、重复生成 | 隐含进程级状态 |
| 并行 | 各线程独立生成器 | 非预期共享计数器 |
| 输出 | 完整与增量三种表示 | 游标和编码不一致 |
建议进一步编写仅支持循环所需标量指令的教学解释器,用它检查执行次数和地址轨迹。对随机小循环生成代码,再与高层循环模拟结果比较。这个实验与黄金测试互补:前者验证抽象行为,后者稳定具体编码。
性能方面,应区分编译器并行吞吐与设备循环性能。消除共享计数器有助于让独立生成更可重复,但不能据此给出设备加速比例。嵌套循环是否优于展开,需要比较代码体积、分支开销、描述符更新和指令缓存压力。
当下一次测试“单独跑就过”,不要只盯线程锁。先问一句:这份程序的标签,为什么记得上一份程序来过?
If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. All the images used in the blog are my original works or AI works, if you want to take it,don't hesitate. Thank you !