SIMD
在追求极致性能的道路上,多线程利用了“更多”的核心,而 SIMD (Single Instruction, Multiple Data) 技术则致力于榨干“每个”核心的计算潜力。SIMD 是一种并行计算的形式,但它发生在比多线程更低的硬件层面上。现代 CPU 的寄存器(CPU内部最快的数据存储单元)通常是128位、256位甚至512位宽,而一个标准的单精度浮点数(float)只有32位。这意味着一个128位的寄存器可以同时容纳4个浮点数。SIMD 指令集(如 Intel 的 SSE/AVX 或 ARM 的 NEON)允许我们用一条指令,对这4个浮点数同时执行相同的操作(如加法、乘法)。在物理引擎中,充斥着大量对3D向量和4x4矩阵的运算,这使其成为应用 SIMD 优化的完美场景。通过将数据打包并使用 SIMD 指令,我们可以获得数倍的性能提升。
1. SIMD 的核心概念
想象一下,你需要计算两个3D向量的和:c = a + b。
传统(标量)做法:
struct Vector3 { float x, y, z; };
Vector3 a, b, c;
// ...
c.x = a.x + b.x; // 1次加法
c.y = a.y + b.y; // 1次加法
c.z = a.z + b.z; // 1次加法
// 总共需要 3 条独立的加法指令
SIMD 做法: 假设我们使用128位的 SIMD 寄存器(可以容纳4个 float)。
-
数据加载 (Load): 将向量
a和b的分量加载到 SIMD 寄存器中。由于向量只有3个分量,我们通常会将第4个分量(w)置为0。__m128 reg_a = _mm_set_ps(0.0f, a.z, a.y, a.x);__m128 reg_b = _mm_set_ps(0.0f, b.z, b.y, b.x); -
SIMD 加法 (Add): 使用一条 SIMD 加法指令,对两个寄存器中的4对浮点数同时进行加法运算。
__m128 reg_c = _mm_add_ps(reg_a, reg_b); -
数据存储 (Store): 将结果从 SIMD 寄存器中存回内存。
_mm_store_ps(&c.x, reg_c);
在这个例子中,我们用一条 _mm_add_ps 指令完成了原本需要3条指令的工作。虽然有数据加载和存储的开销,但在大量的连续计算中,这种收益会累积起来,带来巨大的性能提升。
2. 数据布局的重要性:AoS vs. SoA
SIMD 的效率高度依赖于数据在内存中的布局方式。考虑一个物理引擎,我们需要对N个刚体的状态进行积分。
AoS (Array of Structs) - 传统面向对象布局:
struct RigidBody {
Vector3 position;
Vector3 velocity;
};
RigidBody bodies[N];
在这种布局下,bodies[0].position 和 bodies[1].position 在内存中是不连续的,它们之间隔着 velocity 等其他数据。如果要对4个不同刚体的位置进行 SIMD 操作,CPU 需要从4个不连续的内存地址加载数据,这个过程被称为收集 (Gather),效率很低。
SoA (Struct of Arrays) - 数据导向布局:
struct RigidBodies {
Vector3 positions[N];
Vector3 velocities[N];
};
在这种布局下,所有刚体的位置都连续地存储在一起。当我们要对4个刚体的位置进行 SIMD 操作时,我们可以直接从内存中加载一个连续的块,这个块正好包含 positions[0], positions[1], positions[2], positions[3] 的 x, y, z 分量。这种连续的内存访问模式对 CPU 的缓存和预取器极为友好,并且使得 SIMD 的数据加载操作效率极高。
结论: SoA 布局是释放 SIMD 全部潜力的关键。它将需要一起处理的数据在内存中排列在一起,完美地匹配了 SIMD 的工作方式。
3. 在 C++ 中使用 SIMD:内在函数 (Intrinsics)
直接编写汇编语言来使用 SIMD 是非常困难且不可移植的。因此,编译器提供商(如 Intel, ARM)提供了一种更高层次的抽象:内在函数 (Intrinsics)。
- 什么是内在函数? 它们看起来像普通的 C++ 函数,但编译器会将它们直接翻译成一条或几条特定的 SIMD 汇编指令。
- 示例 (SSE):
__m128: 一种数据类型,代表一个128位的 SIMD 寄存器。_mm_load_ps(float* p): 从地址p加载4个 float 到一个__m128寄存器中(地址必须16字节对齐)。_mm_add_ps(__m128 a, __m128 b): 将两个寄存器中的4对 float 分别相加。_mm_mul_ps(__m128 a, __m128 b): 乘法。_mm_sub_ps(__m128 a, __m128 b): 减法。_mm_store_ps(float* p, __m128 a): 将寄存器a中的4个 float 存回地址p。
通过使用内在函数,开发者可以在 C++ 代码中直接利用 SIMD 的强大能力,同时将平台相关的汇编指令细节交给编译器处理。
4. 物理引擎中的应用场景
- 向量/矩阵运算: 几乎所有的3D向量和4x4矩阵运算(加、减、点积、叉积、矩阵-向量乘法、矩阵-矩阵乘法)都可以被 SIMD 大幅加速。
- 积分: 在积分阶段,更新N个刚体的位置和速度
(pos += vel * dt)是一个完美的数据并行 SIMD 任务。 - AABB 计算: 在宽相中,计算变换后模型的轴对齐包围盒 (AABB) 也涉及到大量的向量和矩阵运算。
- 约束求解: 在求解器中,计算雅可比矩阵、应用冲量等步骤也包含大量的向量运算,可以从 SIMD 中受益。