请不要说这是过早的微观优化。我想了解,尽管我的知识有限,但所描述的SB功能和装配如何工作,并确保我的代码使用这种架构功能。感谢您的理解。
我几天前开始学习内在函数,所以答案对某些人来说似乎显而易见,但我没有可靠的信息来源来解决这个问题。
我需要为Sandy Bridge CPU优化一些代码(这是一项要求)。现在我知道它可以在每个周期进行一次AVX乘法和一次AVX加法,并阅读本文:
http://research.colfaxinternational.com/file.axd?file=2012%2F7%2FColfax_CPI.pdf
显示了如何在C ++中完成它。所以,问题是我的代码不会使用英特尔的编译器自动矢量化(这是任务的另一个要求),所以我决定使用这样的内在函数手动实现它:
__sum1 = _mm256_setzero_pd();
__sum2 = _mm256_setzero_pd();
__sum3 = _mm256_setzero_pd();
sum = 0;
for(kk = k; kk < k + BS && kk < aW; kk+=12)
{
const double *a_addr = &A[i * aW + kk];
const double *b_addr = &newB[jj * aW + kk];
__aa1 = _mm256_load_pd((a_addr));
__bb1 = _mm256_load_pd((b_addr));
__sum1 = _mm256_add_pd(__sum1, _mm256_mul_pd(__aa1, __bb1));
__aa2 = _mm256_load_pd((a_addr + 4));
__bb2 = _mm256_load_pd((b_addr + 4));
__sum2 = _mm256_add_pd(__sum2, _mm256_mul_pd(__aa2, __bb2));
__aa3 = _mm256_load_pd((a_addr + 8));
__bb3 = _mm256_load_pd((b_addr + 8));
__sum3 = _mm256_add_pd(__sum3, _mm256_mul_pd(__aa3, __bb3));
}
__sum1 = _mm256_add_pd(__sum1, _mm256_add_pd(__sum2, __sum3));
_mm256_store_pd(&vsum[0], __sum1);
我在这里解释了手动展开循环的原因:
Loop unrolling to achieve maximum throughput with Ivy Bridge and Haswell
他们说你需要展开3倍才能在桑迪上取得最佳成绩。我的天真测试证实,这确实比没有展开或4倍展开更好。
好的,这就是问题所在。英特尔Parallel Studio 15的icl编译器生成了这个:
$LN149:
movsxd r14, r14d ;78.49
$LN150:
vmovupd ymm3, YMMWORD PTR [r11+r14*8] ;80.48
$LN151:
vmovupd ymm5, YMMWORD PTR [32+r11+r14*8] ;84.49
$LN152:
vmulpd ymm4, ymm3, YMMWORD PTR [r8+r14*8] ;82.56
$LN153:
vmovupd ymm3, YMMWORD PTR [64+r11+r14*8] ;88.49
$LN154:
vmulpd ymm15, ymm5, YMMWORD PTR [32+r8+r14*8] ;86.56
$LN155:
vaddpd ymm2, ymm2, ymm4 ;82.34
$LN156:
vmulpd ymm4, ymm3, YMMWORD PTR [64+r8+r14*8] ;90.56
$LN157:
vaddpd ymm0, ymm0, ymm15 ;86.34
$LN158:
vaddpd ymm1, ymm1, ymm4 ;90.34
$LN159:
add r14d, 12 ;76.57
$LN160:
cmp r14d, ebx ;76.42
$LN161:
jb .B1.19 ; Prob 82% ;76.42
对我而言,这看起来像一团糟,其中正确的顺序(使用方便的SB功能所需的乘法旁边的添加)被打破。
问题:
此汇编代码是否会利用我所指的Sandy Bridge功能?
如果没有,我需要做些什么来利用该功能并防止代码变得像这样“纠结”?
此外,当只有一个循环迭代时,顺序很好而且干净,即加载,乘法,添加,应该是。
答案 0 :(得分:6)
对于x86 CPU,许多人希望从点积中获得最大FLOPS
for(int i=0; i<n; i++) sum += a[i]*b[i];
但事实证明not to be the case。
可以给出最大FLOPS的是
for(int i=0; i<n; i++) sum += k*a[i];
其中k
是常量。为什么CPU没有针对点积进行优化?我可以推测。 CPU优化的一个方面是BLAS。 BLAS正在考虑许多其他程序的构建块。
Level-1和Level-2 BLAS例程在n
增加时变为内存带宽限制。它只是Level-3例程(例如Matrix Multiplication),它们能够被计算限制。这是因为Level-3计算为n^3
,读数为n^2
。因此CPU针对Level-3例程进行了优化。 Level-3例程不需要针对单点产品进行优化。他们每次迭代只需要读取一个矩阵(sum += k*a[i]
)。
由此我们可以得出结论,为了获得Level-3例程的最大FLOPS,每个周期需要读取的位数是
read_size = SIMD_WIDTH * num_MAC
其中num_MAC是每个周期可以完成的乘法累加运算的数量。
SIMD_WIDTH (bits) num_MAC read_size (bits) ports used
Nehalem 128 1 128 128-bits on port 2
Sandy Bridge 256 1 256 128-bits port 2 and 3
Haswell 256 2 512 256-bits port 2 and 3
Skylake 512 2 1024 ?
对于Nehalem-Haswell,这与硬件的能力是一致的。我实际上并不知道Skylake能够在每个时钟周期读取1024位,但如果它不能AVX512将不会非常有趣,所以我对我的猜测充满信心。可以在http://www.anandtech.com/show/6355/intels-haswell-architecture/8
找到每个港口的Nahalem,Sandy Bridge和Haswell的好地块。到目前为止,我已经忽略了延迟和依赖链。要真正获得最大FLOPS,您需要在Sandy Bridge上至少三次展开循环(我使用四个因为我觉得使用三个的倍数不方便)
回答关于性能的问题的最佳方法是找到您期望的操作的理论最佳性能,然后比较您的代码与此接近的程度。我称之为效率。这样做你会发现,尽管重新安排了你在装配中看到的指令,但性能仍然很好。但是您可能需要考虑许多其他微妙的问题。以下是我遇到的三个问题:
l1-memory-bandwidth-50-drop-in-efficiency-using-addresses-which-differ-by-4096
obtaining-peak-bandwidth-on-haswell-in-the-l1-cache-only-getting-62%
difference-in-performance-between-msvc-and-gcc-for-highly-optimized-matrix-multp
我还建议您考虑使用IACA来研究效果。