我一直在尝试改进本地计算机上基本循环的性能。总而言之,我有 2 个大的 float32 切片,并且希望使用任何可能的方法将它们相乘以获得最佳改进。作为参考,我有一个 3.7Ghz AMD 12 核心,运行频率约为 4.1Ghz
首先,for 循环内单个 mul 的基本实现产生:4.2B 操作/秒
基本循环展开产生了相同的结果(go 编译器标准优化看起来对我来说是展开的):
for i := 0; i < len(a); i += 4 {
s0 := a[i] * b[i]
s1 := a[i+1] * b[i+1]
s2 := a[i+2] * b[i+2]
s3 := a[i+3] * b[i+3]
sum += s0 + s1 + s2 + s3
}
如果我在编译器中禁用越界检查,我会看到一个很大的改进:8.2B ops/秒问题是这是编译器默认的安全措施,所以我需要一种方法让编译器知道它不需要执行越界检查,这可以通过循环内的切片容量检查来完成,并提供7.6B操作/秒:
的性能for i := 0; i < len(a) && i < len(b); i += 4 {
aTmp := a[i : i+4 : i+4]
bTmp := b[i : i+4 : i+4]
s0 := aTmp[0] * bTmp[0]
s1 := aTmp[1] * bTmp[1]
s2 := aTmp[2] * bTmp[2]
s3 := aTmp[3] * bTmp[3]
sum += s0 + s1 + s2 + s3
接下来我想走SIMD路线,并首先通过“github.com/bjwbell/gensimd/simd”库实现它:
for i := 0; i < len(a); i += 4 {
a := simd.MulF32x4(simd.F32x4{a[i], a[i+1], a[i+2], a[i+3]}, simd.F32x4{b[i], b[i+1], b[i+2], b[i+3]})
sum += a[0] + a[1] + a[2] + a[3]
}
理论上这应该是在 256 个宽寄存器上对每条指令执行 4 个乘法。结果显示只有 1.1b 次操作/秒,所以显然出了问题
我也使用 cgo 和 assembly 做了同样的事情:
转到文件
C.add_arrays((*C.float)(unsafe.Pointer(&a[0])), (*C.float)(unsafe.Pointer(&b[0])), C.int(len(a)))
c 文件:
void add_arrays(float* a, float* b, int len) {
__m256 va, vb, vsum;
for (int i = 0; i < len; i += 8) {
va = _mm256_load_ps(a + i);
vb = _mm256_load_ps(b + i);
vsum = _mm256_add_ps(va, vb);
_mm256_store_ps(a + i, vsum);
}
}
产生2.9B 操作/秒
我希望 SIMD 比展开版本快几倍,我对 go 实现的编码是否错误或遗漏了什么?我对此比较陌生,所以任何建议都会很好。
看起来编译器正在分解乘法,因为结果和没有保存/使用。修复所有四个测试以存储和使用总和后,我现在看到使用内在函数从 SIMD 获得更多内联结果。即使使用工具/生成/构建,Gensimd lib 似乎也无法工作。
展开 - Bil 操作/秒:2.840884878822056
展开无边界检查 - Bil 操作/秒:2.963691811615894
SIMD gensimd - Bil 操作/秒:0.27859654205971995
SIMD 内在函数 - Bil 操作/秒:4.534921160395626