【问题标题】:When the compiler reorders AVX instructions on Sandy, does it affect performance?当编译器在 Sandy 上重新排序 AVX 指令时,会影响性能吗?
【发布时间】:2015-03-02 10:02:41
【问题描述】:

请不要说这是过早的微优化。鉴于我有限的知识,我想尽可能多地了解所描述的 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 倍才能在 Sandy 上获得最佳性能。我的天真测试证实,这确实比不展开或 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 功能所需的乘法旁边添加)被破坏了。

问题:

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

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

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

【问题讨论】:

  • 我无法从您的问题中判断您是否知道处理器本身能够重新排序指令。所以加法不需要需要在乘法旁边。此外,代码中的瓶颈将是负载。因此,无论如何,您不会从重叠的加法和乘法中获得太多收益。
  • 是的,我知道 CPU 可以重新排序指令,但不知道它何时以及如何准确地这样做。我知道内存是算法中最重要的部分,当然,但是当内存或多或少没问题时,我想确保 FPU 正在全速工作,对吗?
  • FPU 不能在您的示例中满负荷运行。 Sandy Bridge 每个周期只能承受一个 AVX 负载。所以循环至少需要 6 个周期。要使 FPU 饱和,您需要 6 次加法 6 次乘法。但是你每个人只有 3 个 - 所以你永远不会获得超过 50% 的 FPU 吞吐量。
  • 这与展开因素无关。你只是有太多的负载。沙桥,每个周期可以承受 1 个负载,1 个加法和 1 个乘法。但是您需要 2 次加载、1 次加法和 1 次乘法。所以你的瓶颈是负载。
  • 如果您查看我引用的链接中的代码,您会发现其中一个因素在循环中是不变的 (__m256 a8 = _mm256_set1_ps(1.0f);)。如果您在循环之外定义 __aa1 = _mm256_load_pd((a_addr));(或广播一个可能是您真正想要做的值),那么每次多加将只有一个 256 位负载,而不是两个。当然,这会改变你的工作,所以你需要考虑你想做什么,看看是否可行。

标签: c performance optimization intrinsics avx


【解决方案1】:

对于 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 正在考虑构建许多其他例程。

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

【讨论】:

  • 我不会说“如果每个周期不能加载 1024 位,AVX512 就不会有趣”。矩阵乘法并不是唯一的应用程序。我处理的东西具有更高的计算/负载比。但鉴于英特尔似乎确实在针对线性代数优化处理器,因此很难不获得双问题 512 位负载。
  • @Mysticial,你是对的。我应该说这对 BLAS 来说不会很有趣。我认为 DGEMM 是许多人所期望的基准,尤其是在 HPC(Top500)中。因此,为了吹牛,英特尔想要双发 512 位负载。我不知道一般来说强调 BLAS 是否是一件值得优化的好事情。
  • 感谢您的详尽回答,我还没有时间研究所有链接,但很快就会这样做!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-02-22
  • 2017-07-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-12-19
  • 1970-01-01
相关资源
最近更新 更多