本节摘要:约束循环和 AABB 测试是流式运算,适合 SIMD,前提是数据已经按列排好、对齐、分支少。先把 SoA 和预留容量做对,再对“无分支的成对 AABB”或“四条接触同时算相对速度”下手。过早写 intrinsics 只会让热启动和钳制里的分支把向量通道拆空。
阅读完本节,你应当能够:
顺序冲量对每个接触读:两个下标,两个速度,两个角速度,两个 r,法向,K,lambda。若这些从对象指针跳出来,SIMD 没有操作数。SoA 后,至少速度列是连续的。接触自己也 SoA:nX[i] nY[i] nZ[i] lambda[i]。一次迭代扫 lambda 列更新,缓存行利用率高。
AoSoA:四个接触一组,组内 SoA,适合显式 SSE。复杂度上升。物体少于几百时,SoA 标量循环加编译器自动向量化往往够。先看分析器:接触循环是否已经占 40% 以上,再手写。
不好: for c in contacts: Body* a = c.a; // 跳堆 a->v += ... 好: for i in 0..n: ia = idxA[i]; ib = idxB[i] vx[ia] += ... // 仍有间接,但列连续
间接 vx[ia] 仍是 gather,SIMD 收益受限。岛内重排刚体让接触两端下标局部化,gather 更近。这是布局和岛的交叉优化。
| 技巧 | 做什么 | 何时值得 |
|---|---|---|
| SoA | 列存 | 几乎总是,从第 1.3 节 |
| 对齐 16/32 字节 | 避免跨缓存行 | SIMD 前 |
| 去掉每接触 malloc | 预留 | 总是 |
| 形状特化循环 | 球-球一批、盒-盒一批 | 窄相占时高 |
| 手写 SIMD 冲量 | 四接触并行 | 分析器证明且分支可掩码 |
| 虚函数 per shape | 方便 | SIMD 杀手,热路径别用 |
窄相按形状对排序:所有球-球一起,盒子一起。分支预测和指令缓存都受益,也方便给球-球写 SIMD。这比给通用 GJK 向量化现实。
if (lambda < 0) lambda = 0 在 SIMD 里用 max。if (static) skip 会拆掉向量。静态体分表后循环里没有这种 if。休眠用岛跳过整段循环,不要在每个接触里 if 睡着。
自动向量化:写简单的索引循环,指针 restrict,别在循环里调虚函数。看汇编有没有 packed 指令。没有的话,不是“需要立刻 intrinsics”,而是循环还不够干净。
💡 关键直觉:SIMD 吃的是“同样的指令、整齐的数据”。多态形状、脚本回调、printf 都是反 SIMD。热路径只留算术和钳制。
假共享:两个核写相邻的 lambda,同一缓存行来回作废。按岛分割后每核写自己的接触区间,区间按缓存行对齐填充。单线程无此问题,布局时预留 padding 字段给线程 id 范围。
只积分 10 万粒子,几乎不碰撞:若耗时随数组变大线性涨且接近内存带宽,你是带宽墙,SIMD 算力帮不上太多,反而可能因读得更宽更堵。此时减字段、量化、别用 vec4 浪费。约束迭代很多、物体中等:计算墙,SIMD 和少迭代(热启动)有用。
⚠️ 常见坑:为对齐把 vec3 改 vec4,带宽 +33%,物体很多时更慢。对齐用分配器对齐起始地址,不要盲目加填充到每个向量。
度量清单:每步宽相时间、窄相时间、求解时间、积分时间。哪项过半砍哪项。求解过半先热启动和岛休眠;宽相过半先格边长和掩码;窄相过半先形状分桶和快路径。SIMD 是在确认循环干净之后的最后一成。
不要在 Lua 里更新每个刚体位置再写回,那是缓存和语言边界双重税。物理在原生循环跑完,脚本只读快照。
测试:SoA 改造前后,五千球自由落体应明显加快;加碰撞后看求解是否成为新热点。没有前后数字的“优化”不算完成。
编译器开关和快速数学:物理里 NaN 要暴露,别全局快数学把 denormal 当零搞出诡异休眠。局部循环可以放宽。确定性要求固定舍入,SIMD 归约顺序变化会改末位,联机项目要么单线程求解要么按岛固定归约顺序——见 7.2。性能和确定性在 SIMD 归约上打架,产品必须选边。
指令集:SSE2 基线够用。为 AVX512 写两套维护成本通常不值,除非粒子 10 万+。优先算法和布局。
写一个只做球-球距离平方比较的循环,一万对,看汇编是否出现打包指令。没有就检查:循环内是否调用函数、是否取地址逃逸、下标是否复杂。干净之后再谈手写。手写 SSE 的球-球可以当练习,但要有标量对照测试,浮点末位允许差一点,符号和候选集合不能差。
热路径禁令清单:虚函数、异常、锁、内存分配、日志、脚本回调、文件系统、取时间。违反一条,SIMD 和缓存都白做。回调改到 step 末尾队列。日志用环形缓冲,调试时开。
AoSoA 四接触一组:lambda 钳制用 max,休眠不在组内分支,整组跳过或整组算。组内有静态体就浪费通道,所以静态分表更重要。先分表,再 AoSoA。顺序反了,通道里一半是零质量的空算。
测量对比:同一场景,标量 SoA vs 自动向量化 vs 手写。收益小于 10% 就停在自动,把时间拿去减候选对数。工程师喜欢写 intrinsics,分析器往往说宽相或睡眠更值。听分析器。
先看是宽相候选太多、窄相虚函数、还是求解迭代。SIMD 是循环干净之后的最后一成。先分桶、去虚函数、SoA。
有限。岛内重排让下标局部化更值。先局部性,再打包指令。
日志分配、锁、格式化,把向量通道和缓存一起毁掉,还可能引入非确定性时钟。环形缓冲,步末刷。
对齐带来的收益大于 33% 多余带宽时。物体很多时往往输。测,不要信仰对齐。
循环不干净:函数调用、逃逸、复杂下标。先把循环写丑而干净,再谈 intrinsics。
SIMD 杀手。按形状分桶。
实时卡顿老朋友。预留。
语言边界税。脚本读快照。
休眠诡异。局部再放宽。
通常不值。先算法布局。
停,去减候选。听分析器。
通道空算。先分表再 AoSoA。
多核写炸缓存行。岛切片对齐。
对账数字比观感先行。静止支撑冲量是否接近质量乘重力乘 dt,自由落体一秒是否接近四点九米,两球对心是否交换速度。「01-SIMD与指令级优化」相关实现先过对账,再谈好看。 本条对应「01-SIMD与指令级优化」第 1 号备忘,和相邻条目不要合并成一句空话,分开验收。
「01-SIMD与指令级优化」出抖动时按清单:时间步是否固定、流形点是否闪、法向是否跳、恢复系数是否在静止时仍生效、bias 是否过大、迭代是否其实在清零热启动。不要先把摩擦系数调到十。 本条对应「01-SIMD与指令级优化」第 2 号备忘,和相邻条目不要合并成一句空话,分开验收。
空间结构必须在世界系。把「01-SIMD与指令级优化」绑到摄像机或屏幕格子,逻辑和确定性都会在转视角时崩。渲染剔除不能代替宽相,看不见的物体仍能被撞到。 本条对应「01-SIMD与指令级优化」第 3 号备忘,和相邻条目不要合并成一句空话,分开验收。
「01-SIMD与指令级优化」的预分配策略:按关卡上限留容量,超出打日志并降级,不要在 step 中途默默扩容。扩容是卡顿尖峰,也是回放时分配顺序不同导致的潜在分叉源。 本条对应「01-SIMD与指令级优化」第 4 号备忘,和相邻条目不要合并成一句空话,分开验收。
把「01-SIMD与指令级优化」相关的失败做成最小复现:关掉渲染特效、关掉音频、只留固定 dt 的 step 和一份输入日志。能在十秒内重放出来的 bug 才叫被抓住。不能重放就先补录制,再谈修。很多人在这一步省时间,后面用几天陪着偶发抖动。 本条对应「01-SIMD与指令级优化」第 5 号备忘,和相邻条目不要合并成一句空话,分开验收。
在「01-SIMD与指令级优化」路径上加计数器要进 step 末尾快照,不要在热循环里格式化字符串。计数器本身若分配内存,你会优化一个被测量污染的世界。发布版可用宏剥掉绘制,但计数器的定义要保留,方便线上开一个极轻的统计开关。 本条对应「01-SIMD与指令级优化」第 6 号备忘,和相邻条目不要合并成一句空话,分开验收。
「01-SIMD与指令级优化」一旦和随机数沾边,随机必须绑在 tick 上。墙钟、哈希表遍历、线程完成顺序都是隐藏输入。隐藏输入会让哈希校验在某一帧突然红,而你却以为是公式写错。先排除隐藏输入,再怀疑有效质量。 本条对应「01-SIMD与指令级优化」第 7 号备忘,和相邻条目不要合并成一句空话,分开验收。
调「01-SIMD与指令级优化」时一次只改一个量,写下场景名、旧值、新值、醒岛数或能量变化。没有笔记的调参会在一周后变成传说。传说不能回归,不能交给同事,也不能在换库时当合同。 本条对应「01-SIMD与指令级优化」第 8 号备忘,和相邻条目不要合并成一句空话,分开验收。
「01-SIMD与指令级优化」的单元测试尽量不依赖图形窗口。能在无头模式跑的断言,才会每次提交都跑。只靠进游戏看一看,等于没有测试。把金字塔睡眠、速度交换、自由落体距离这类金标准写成断言。 本条对应「01-SIMD与指令级优化」第 9 号备忘,和相邻条目不要合并成一句空话,分开验收。
文档里的公式符号必须和代码注释同一张表。尤其是法向从谁指向谁、相对速度谁减谁、lambda 是力还是冲量。「01-SIMD与指令级优化」相关代码若混用两套符号,吸引接触和飞天会轮流出现,现场非常像随机 bug。 本条对应「01-SIMD与指令级优化」第 10 号备忘,和相邻条目不要合并成一句空话,分开验收。
性能上先问能不能少做。「01-SIMD与指令级优化」再快,也快不过把睡着的岛整岛跳过、把碎片到期删掉、把静态体移出动态表。减工作量优于把同样工作向量化。向量化平方算法只是错得更稳更快。 本条对应「01-SIMD与指令级优化」第 11 号备忘,和相邻条目不要合并成一句空话,分开验收。
下一节把接触图切成岛,让多核有活干,也让睡着的岛零成本。