当编译器在Sandy上重新排序AVX指令时,它是否会影响性能?

iks*_*nov 15 c optimization performance intrinsics avx

请不要说这是过早的微观优化.我想了解,尽管我的知识有限,但所描述的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);
Run Code Online (Sandbox Code Playgroud)

我在这里解释了手动展开循环的原因:

循环展开以实现Ivy Bridge和Haswell的最大吞吐量

他们说你需要展开3倍才能在Sandy上取得最佳成绩.我天真的测试证实,这确实比没有展开或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
Run Code Online (Sandbox Code Playgroud)

对我来说,这看起来像一团糟,其中正确的顺序(使用方便的SB功能所需的乘法旁边添加)被打破.

题:

  • 这个汇编代码是否会利用我所指的Sandy Bridge功能?

  • 如果没有,我需要做什么才能利用该功能并防止代码变得像这样"纠结"?

此外,当只有一个循环迭代时,顺序很好而且干净,即加载,乘法,添加,应该是.

Z b*_*son 6

对于x86 CPU,许多人希望从dot产品中获得最大的FLOPS

for(int i=0; i<n; i++) sum += a[i]*b[i];
Run Code Online (Sandbox Code Playgroud)

但结果并非如此.

什么可以给出最大的FLOPS是这个

for(int i=0; i<n; i++) sum += k*a[i];
Run Code Online (Sandbox Code Playgroud)

哪里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
Run Code Online (Sandbox Code Playgroud)

其中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              ?
Run Code Online (Sandbox Code Playgroud)

对于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-4089.

获得峰值带宽上,Haswell的内式L1高速缓存,仅-获得,62%

msvc-and-gcc-for-high-optimized-matrix-multp之间的性能差异.

我还建议您考虑使用IACA来研究性能.