【发布时间】:2014-11-13 07:43:49
【问题描述】:
我尝试使用 GCC 和内在函数优化许多紧密循环。例如下面的函数。
void triad(float *x, float *y, float *z, const int n) {
float k = 3.14159f;
int i;
__m256 k4 = _mm256_set1_ps(k);
for(i=0; i<n; i+=8) {
_mm256_store_ps(&z[i], _mm256_add_ps(_mm256_load_ps(&x[i]), _mm256_mul_ps(k4, _mm256_load_ps(&y[i]))));
}
}
这会产生一个像这样的主循环
20: vmulps ymm0,ymm1,[rsi+rax*1]
25: vaddps ymm0,ymm0,[rdi+rax*1]
2a: vmovaps [rdx+rax*1],ymm0
2f: add rax,0x20
33: cmp rax,rcx
36: jne 20
但是cmp 指令是不必要的。我们可以将基指针(rsi、rdi 和 rdx)设置到数组的末尾并将 rax 设置为 -sizeof(float)*n,而不是让 rax 从零开始并在 sizeof(float)*n 结束然后测试为零。我可以用我自己的汇编代码来做到这一点
.L2 vmulps ymm1, ymm2, [rdi+rax]
vaddps ymm0, ymm1, [rsi+rax]
vmovaps [rdx+rax], ymm0
add rax, 32
jne .L2
但我无法让 GCC 做到这一点。我现在有几个测试,这有很大的不同。直到最近 GCC 和内在函数已经很好地切断了我,所以我想知道是否有编译器开关或重新排序/更改我的代码的方法,因此 cmp 指令不是由 GCC 生成的。
我尝试了以下方法,但它仍然产生cmp。我尝试过的所有变体仍然产生cmp。
void triad2(float *x, float *y, float *z, const int n) {
float k = 3.14159f;
float *x2 = x+n;
float *y2 = y+n;
float *z2 = z+n;
int i;
__m256 k4 = _mm256_set1_ps(k);
for(i=-n; i<0; i+=8) {
_mm256_store_ps(&z2[i], _mm256_add_ps(_mm256_load_ps(&x2[i]), _mm256_mul_ps(k4, _mm256_load_ps(&y2[i]))));
}
}
编辑:
我有兴趣为适合 L1 缓存的数组(实际上是 n=2048)最大化这些函数的指令级并行性 (ILP)。虽然展开可用于提高带宽,但它会降低 ILP(假设无需展开即可获得全部带宽)。
编辑:
这是 Core2(Nehalem 之前)、IvyBridge 和 Haswell 系统的结果表。 Intrinsics 是使用intrinsic 的结果,unroll1 是我没有使用cmp 的汇编代码,unroll16 是我的汇编代码展开16 次。百分比是峰值性能的百分比(frequency*num_bytes_cycle,其中 num_bytes_cycle 对于 SSE 是 24,对于 AVX 是 48,对于 FMA 是 96)。
SSE AVX FMA
intrinsic 71.3% 90.9% 53.6%
unroll1 97.0% 96.1% 63.5%
unroll16 98.6% 90.4% 93.6%
ScottD 96.5%
32B code align 95.5%
对于 SSE,我在不展开的情况下得到的结果几乎与展开一样好,但前提是我不使用 cmp。在 AVX 上,我在不展开和不使用 cmp 的情况下获得最佳结果。有趣的是,在 IB 上展开实际上更糟。在 Haswell 上,我通过展开获得了迄今为止最好的结果。这就是为什么我问这个question。可以在那个问题中找到测试它的源代码。
编辑:
根据 ScottD 的回答,现在我的 Core2 系统(Nehalem 64 位之前的模式)的内部函数几乎达到了 97%。 我不确定为什么 cmp 确实很重要,因为它应该无论如何,每次迭代需要 2 个时钟周期。对于 Sandy Bridge,事实证明效率损失是由于代码对齐而不是额外的cmp。无论如何,在 Haswell 上只能展开工作。
【问题讨论】:
-
有些事情告诉我,可能应该比现在更多地展开循环。
-
@Zboson:啊,是的,我现在看到了。我不知道如何告诉
gcc避免cmp。 Clang 用 tst 替换了第二个中的 cmp,但这并没有多大帮助。 (终止条件不应该是i < 0吗?) -
您检查过性能吗?我怀疑您是否能够检测到两个版本之间的差异,因为数据访问的数量是相同的。除非您有非常专业的用例,否则访问内存几乎总是性能瓶颈。
-
为了清楚起见,我将第一个 GCC 版本的性能与我在汇编中编写的版本(使用 NASM)进行了比较。
-
只是提醒一下,我找到了一种在 gcc 中无需内在函数(只是内置函数,这显然更好,对吧?)的方法。跨度>
标签: c gcc optimization assembly intrinsics