当编译器对 Sandy 上的 AVX 指令重新排序时,它会影响性能吗?

When the compiler reorders AVX instructions on Sandy, does it affect performance?

请不要说这是过早的微优化。鉴于我有限的知识,我想尽可能多地了解所描述的 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 倍展开运行得更好。

好的,问题来了。来自 Intel 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 功能需要在乘法旁边添加)被破坏了。

问题:

此外,当只有一个循环迭代时,顺序很好,很干净,即加载、乘法、加法,这是应该的。

对于 x86 CPUs,许多人期望从点积中获得最大的 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 正在考虑许多其他例程的构建块。

随着 n 的增加,Level-1 和 Level-2 BLAS 例程成为内存带宽限制。只有 Level-3 例程(例如矩阵乘法)能够受计算限制。这是因为 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 上至少展开循环 3 次(我使用 4 次是因为我发现使用 3 的倍数不方便)

回答有关性能问题的最佳方法是找到您期望的理论最佳操作性能,然后比较您的代码与该性能的接近程度。我称之为效率。这样做你会发现,尽管你在程序集中看到的指令重新排序,但性能仍然很好。但是您可能需要考虑许多其他微妙的问题。以下是我遇到的三个问题:

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来研究性能。