【发布时间】: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