6.1 SIMD 与数据布局优化


6.1 SIMD 与数据布局优化

本节摘要:约束循环和 AABB 测试是流式运算,适合 SIMD,前提是数据已经按列排好、对齐、分支少。先把 SoA 和预留容量做对,再对“无分支的成对 AABB”或“四条接触同时算相对速度”下手。过早写 intrinsics 只会让热启动和钳制里的分支把向量通道拆空。

读前必看

阅读完本节,你应当能够:

  1. 对比 AoS 与 SoA 在接触循环上的缓存行为
  2. 列出 SIMD 友好代码的条件:对齐、少分支、结构一致
  3. 用编译器自动向量化试水,再决定是否手写
  4. 避免在脚本回调和多态形状虚函数上幻想 SIMD

一、先问扫描的是哪几列

顺序冲量对每个接触读:两个下标,两个速度,两个角速度,两个 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 吗?

先看是宽相候选太多、窄相虚函数、还是求解迭代。SIMD 是循环干净之后的最后一成。先分桶、去虚函数、SoA。

gather 间接下标还有 SIMD 收益吗?

有限。岛内重排让下标局部化更值。先局部性,再打包指令。

为什么热路径禁止日志?

日志分配、锁、格式化,把向量通道和缓存一起毁掉,还可能引入非确定性时钟。环形缓冲,步末刷。

vec4 填充什么时候赢?

对齐带来的收益大于 33% 多余带宽时。物体很多时往往输。测,不要信仰对齐。

自动向量化没有 packed 指令说明什么?

循环不干净:函数调用、逃逸、复杂下标。先把循环写丑而干净,再谈 intrinsics。

参数扫描笔记:SIMD

扫描 1:热路径虚函数

SIMD 杀手。按形状分桶。

扫描 2:循环内 malloc

实时卡顿老朋友。预留。

扫描 3:脚本每体写回位置

语言边界税。脚本读快照。

扫描 4:全局快数学藏 NaN

休眠诡异。局部再放宽。

扫描 5:为 AVX512 维护两套

通常不值。先算法布局。

扫描 6:收益小于百分之十仍手写

停,去减候选。听分析器。

扫描 7:组内混静态体

通道空算。先分表再 AoSoA。

扫描 8:假共享计数器相邻

多核写炸缓存行。岛切片对齐。

工程备忘录

备忘 1

对账数字比观感先行。静止支撑冲量是否接近质量乘重力乘 dt,自由落体一秒是否接近四点九米,两球对心是否交换速度。「01-SIMD与指令级优化」相关实现先过对账,再谈好看。 本条对应「01-SIMD与指令级优化」第 1 号备忘,和相邻条目不要合并成一句空话,分开验收。

备忘 2

「01-SIMD与指令级优化」出抖动时按清单:时间步是否固定、流形点是否闪、法向是否跳、恢复系数是否在静止时仍生效、bias 是否过大、迭代是否其实在清零热启动。不要先把摩擦系数调到十。 本条对应「01-SIMD与指令级优化」第 2 号备忘,和相邻条目不要合并成一句空话,分开验收。

备忘 3

空间结构必须在世界系。把「01-SIMD与指令级优化」绑到摄像机或屏幕格子,逻辑和确定性都会在转视角时崩。渲染剔除不能代替宽相,看不见的物体仍能被撞到。 本条对应「01-SIMD与指令级优化」第 3 号备忘,和相邻条目不要合并成一句空话,分开验收。

备忘 4

「01-SIMD与指令级优化」的预分配策略:按关卡上限留容量,超出打日志并降级,不要在 step 中途默默扩容。扩容是卡顿尖峰,也是回放时分配顺序不同导致的潜在分叉源。 本条对应「01-SIMD与指令级优化」第 4 号备忘,和相邻条目不要合并成一句空话,分开验收。

备忘 5

把「01-SIMD与指令级优化」相关的失败做成最小复现:关掉渲染特效、关掉音频、只留固定 dt 的 step 和一份输入日志。能在十秒内重放出来的 bug 才叫被抓住。不能重放就先补录制,再谈修。很多人在这一步省时间,后面用几天陪着偶发抖动。 本条对应「01-SIMD与指令级优化」第 5 号备忘,和相邻条目不要合并成一句空话,分开验收。

备忘 6

在「01-SIMD与指令级优化」路径上加计数器要进 step 末尾快照,不要在热循环里格式化字符串。计数器本身若分配内存,你会优化一个被测量污染的世界。发布版可用宏剥掉绘制,但计数器的定义要保留,方便线上开一个极轻的统计开关。 本条对应「01-SIMD与指令级优化」第 6 号备忘,和相邻条目不要合并成一句空话,分开验收。

备忘 7

「01-SIMD与指令级优化」一旦和随机数沾边,随机必须绑在 tick 上。墙钟、哈希表遍历、线程完成顺序都是隐藏输入。隐藏输入会让哈希校验在某一帧突然红,而你却以为是公式写错。先排除隐藏输入,再怀疑有效质量。 本条对应「01-SIMD与指令级优化」第 7 号备忘,和相邻条目不要合并成一句空话,分开验收。

备忘 8

调「01-SIMD与指令级优化」时一次只改一个量,写下场景名、旧值、新值、醒岛数或能量变化。没有笔记的调参会在一周后变成传说。传说不能回归,不能交给同事,也不能在换库时当合同。 本条对应「01-SIMD与指令级优化」第 8 号备忘,和相邻条目不要合并成一句空话,分开验收。

备忘 9

「01-SIMD与指令级优化」的单元测试尽量不依赖图形窗口。能在无头模式跑的断言,才会每次提交都跑。只靠进游戏看一看,等于没有测试。把金字塔睡眠、速度交换、自由落体距离这类金标准写成断言。 本条对应「01-SIMD与指令级优化」第 9 号备忘,和相邻条目不要合并成一句空话,分开验收。

备忘 10

文档里的公式符号必须和代码注释同一张表。尤其是法向从谁指向谁、相对速度谁减谁、lambda 是力还是冲量。「01-SIMD与指令级优化」相关代码若混用两套符号,吸引接触和飞天会轮流出现,现场非常像随机 bug。 本条对应「01-SIMD与指令级优化」第 10 号备忘,和相邻条目不要合并成一句空话,分开验收。

备忘 11

性能上先问能不能少做。「01-SIMD与指令级优化」再快,也快不过把睡着的岛整岛跳过、把碎片到期删掉、把静态体移出动态表。减工作量优于把同样工作向量化。向量化平方算法只是错得更稳更快。 本条对应「01-SIMD与指令级优化」第 11 号备忘,和相邻条目不要合并成一句空话,分开验收。

要点串联

  • SoA 和预留容量先于 intrinsics
  • 热路径按形状分桶,去掉虚函数和回调
  • 钳制用 max/min 以便向量化;休眠按岛跳过不是按接触 if。
  • 先测带宽还是计算,再决定 SIMD 还是减数据。
  • vec4 填充可能更慢;对齐起始地址即可。
  • 脚本不进热循环
  • SIMD 归约顺序影响确定性,联机要固定。

下一节把接触图切成岛,让多核有活干,也让睡着的岛零成本。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U