我正在寻找一种更快、更巧妙的方法来在C语言中计算两个4x4矩阵的乘积。我的当前研究重点是基于x86-64汇编语言和SIMD扩展。到目前为止,我已经创建了一个函数,比朴素的C实现快了大约6倍,这已经超出了我对性能提升的期望。不幸的是,只有在没有使用编译优化标志(GCC 4.7)时才能保持这种速度。使用
我知道现代编译器利用复杂的优化技术可以实现几乎完美的代码,通常比手工编写的巧妙汇编代码更快。但在少数性能关键的情况下,人类可能会试图与编译器竞争时钟周期。特别是当某些基于现代ISA的数学问题可以被探索时(正如我的情况)。
我的函数如下(AT&T语法,GNU汇编器):
我已经调查了上述 C 代码的优化汇编输出,它在将浮点数存储在 XMM 寄存器中时,并没有涉及任何并行操作——只有标量计算、指针算术和条件跳转。编译器的代码似乎不够明确,但仍然比我的向量化版本略微更有效,后者预计会快大约 4 倍。我相信这个一般想法是正确的——程序员用类似的方法获得了回报。但是这里有什么问题吗?我是否意识到寄存器分配或指令调度问题?你知道任何 x86-64 汇编工具或技巧来支持我对抗机器吗?
-O2
后,C变得更快,我的努力变得毫无意义。我知道现代编译器利用复杂的优化技术可以实现几乎完美的代码,通常比手工编写的巧妙汇编代码更快。但在少数性能关键的情况下,人类可能会试图与编译器竞争时钟周期。特别是当某些基于现代ISA的数学问题可以被探索时(正如我的情况)。
我的函数如下(AT&T语法,GNU汇编器):
.text
.globl matrixMultiplyASM
.type matrixMultiplyASM, @function
matrixMultiplyASM:
movaps (%rdi), %xmm0 # fetch the first matrix (use four registers)
movaps 16(%rdi), %xmm1
movaps 32(%rdi), %xmm2
movaps 48(%rdi), %xmm3
xorq %rcx, %rcx # reset (forward) loop iterator
.ROW:
movss (%rsi), %xmm4 # Compute four values (one row) in parallel:
shufps $0x0, %xmm4, %xmm4 # 4x 4FP mul's, 3x 4FP add's 6x mov's per row,
mulps %xmm0, %xmm4 # expressed in four sequences of 5 instructions,
movaps %xmm4, %xmm5 # executed 4 times for 1 matrix multiplication.
addq $0x4, %rsi
movss (%rsi), %xmm4 # movss + shufps comprise _mm_set1_ps intrinsic
shufps $0x0, %xmm4, %xmm4 #
mulps %xmm1, %xmm4
addps %xmm4, %xmm5
addq $0x4, %rsi # manual pointer arithmetic simplifies addressing
movss (%rsi), %xmm4
shufps $0x0, %xmm4, %xmm4
mulps %xmm2, %xmm4 # actual computation happens here
addps %xmm4, %xmm5 #
addq $0x4, %rsi
movss (%rsi), %xmm4 # one mulps operand fetched per sequence
shufps $0x0, %xmm4, %xmm4 # |
mulps %xmm3, %xmm4 # the other is already waiting in %xmm[0-3]
addps %xmm4, %xmm5
addq $0x4, %rsi # 5 preceding comments stride among the 4 blocks
movaps %xmm5, (%rdx,%rcx) # store the resulting row, actually, a column
addq $0x10, %rcx # (matrices are stored in column-major order)
cmpq $0x40, %rcx
jne .ROW
ret
.size matrixMultiplyASM, .-matrixMultiplyASM
它通过处理128位SSE寄存器中打包的四个浮点数来计算每次迭代的整个结果矩阵列。通过进行一些数学运算(操作重新排序和聚合)和使用mullps
/addps
指令并行乘法/加法4xfloat包,可以实现完全矢量化。该代码重用了用于传递参数的寄存器(%rdi
、%rsi
、%rdx
:GNU/Linux ABI),受益于(内部)循环展开,并在XMM寄存器中完全保存一个矩阵以减少内存读取。正如您所看到的,我已经研究了这个主题,并花费了时间尽可能地实现它。
征服我的代码的朴素C计算如下:
void matrixMultiplyNormal(mat4_t *mat_a, mat4_t *mat_b, mat4_t *mat_r) {
for (unsigned int i = 0; i < 16; i += 4)
for (unsigned int j = 0; j < 4; ++j)
mat_r->m[i + j] = (mat_b->m[i + 0] * mat_a->m[j + 0])
+ (mat_b->m[i + 1] * mat_a->m[j + 4])
+ (mat_b->m[i + 2] * mat_a->m[j + 8])
+ (mat_b->m[i + 3] * mat_a->m[j + 12]);
}
我已经调查了上述 C 代码的优化汇编输出,它在将浮点数存储在 XMM 寄存器中时,并没有涉及任何并行操作——只有标量计算、指针算术和条件跳转。编译器的代码似乎不够明确,但仍然比我的向量化版本略微更有效,后者预计会快大约 4 倍。我相信这个一般想法是正确的——程序员用类似的方法获得了回报。但是这里有什么问题吗?我是否意识到寄存器分配或指令调度问题?你知道任何 x86-64 汇编工具或技巧来支持我对抗机器吗?
restrict
限定符并使用-O3
编译,GCC 将对其进行矢量化处理。如果没有restrict
限定符,编译器必须假设输出矩阵可能与输入矩阵之一相同。 - caf