当编译器对 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 功能需要在乘法旁边添加)被破坏了。
问题:
此汇编代码是否会利用我所指的 Sandy Bridge 功能?
如果不是,我需要做什么才能利用这个功能,防止代码变成这样"tangled"?
此外,当只有一个循环迭代时,顺序很好,很干净,即加载、乘法、加法,这是应该的。
对于 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来研究性能。
请不要说这是过早的微优化。鉴于我有限的知识,我想尽可能多地了解所描述的 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 功能需要在乘法旁边添加)被破坏了。
问题:
此汇编代码是否会利用我所指的 Sandy Bridge 功能?
如果不是,我需要做什么才能利用这个功能,防止代码变成这样"tangled"?
此外,当只有一个循环迭代时,顺序很好,很干净,即加载、乘法、加法,这是应该的。
对于 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来研究性能。