TL;DR我有一个循环需要 1 个周期才能在 Skylake 上执行(它执行 3 次加法 + 1 次增量/跳转)。
当我将其展开超过 2 次(无论多少次)时,我的程序运行速度会慢 25% 左右。这可能与对齐有关,但我不清楚是什么。
编辑:这个问题曾经询问为什么微指令是由 DSB 而不是 MITE 提供的。现在已移至这个问题 https://stackoverflow.com/q/59936512/4990392.
我试图对一个循环进行基准测试,该循环在我的 Skylake 上进行了 3 个添加。这个循环应该在一个周期内执行,因为3加+1增量与条件跳转融合在一起,一旦融合就可以在一个周期内执行。正如预期的那样。
然而,在某些时候,我的 C 编译器尝试展开该循环,从而产生更差的性能。我现在试图理解为什么展开的循环比未展开的循环具有更差的性能,因为我期望两者具有相同的性能,或者展开的循环可能小于慢 15% https://stackoverflow.com/a/39940932/4990392.
这是我的 C 代码:
int main() {
int a, b, c, d;
#pragma unroll(2)
for (unsigned long i = 0; i < 2000000000; i++) {
asm volatile("" : "+r" (a), "+r" (b), "+r" (c), "+r" (d));
a = a + d;
b = b + d;
c = c + d;
}
// Prevent data from being optimized out
asm volatile("" : "+r" (a), "+r" (b), "+r" (c));
}
使用 Clang 7.0.0 -O3 进行编译会生成以下(已清理的)程序集(称为v1
今后):
movl $2000000000, %esi
.p2align 4, 0x90
.LBB0_1:
addl %edi, %edx
addl %edi, %ecx
addl %edi, %eax
addl %edi, %edx
addl %edi, %ecx
addl %edi, %eax
addq $-2, %rsi
jne .LBB0_1
并进行基准测试perf stat -e cycles
表明每次迭代大约需要 2 个周期。
然而,用“新的 64 位寄存器”(r8 到 r15)替换任何寄存器会导致循环在 3 个周期内执行,而不是 2 个周期(我们称此代码为v2
):
movl $2000000000, %esi
.p2align 4, 0x90
.LBB0_1:
addl %edi, %r14d
addl %edi, %ecx
addl %edi, %eax
addl %edi, %r14d
addl %edi, %ecx
addl %edi, %eax
addq $-2, %rsi
jne .LBB0_1
这不是一个随机的例子:如果我向程序添加一些东西并且运气不好,Clang 实际上会产生这个循环(我的初始版本是相同的 C 代码,带有额外的变量随机初始化、预热阶段和 rdtscp 来计时)循环,并使用了 Clangr14d
在循环)。该循环大约执行 3 个周期/迭代。
进一步的测试表明,展开循环任意次数大于 2 次都会使程序执行 25 亿个周期(而不展开的循环为 20 亿个周期)。
一个循环中的uop数为3*n+1
(where n
是展开因子,并且1
代表融合的add/jne
),这意味着展开3次的循环有10个uop; 4 乘以 13 微指令等。这些是相当少量的微指令,应该适合 DSB(微指令高速缓存)。我在 Skylake 上更新了微代码并修复了 SKL150,因此我的 LSD 循环缓冲区被禁用 https://stackoverflow.com/questions/45660139/how-exactly-do-partial-registers-on-haswell-skylake-perform-writing-al-seems-to/45660140#45660140.
此外,展开 3、4、10 或 50 次根本不会改变性能:我的代码始终运行 25 亿个周期(而非展开的代码运行 20 亿个周期)。这有点令人惊讶,因为 3 个加法应该总是在 1 个周期中执行,因此,如果由于某种原因在循环末尾丢失了一个额外的周期,则在展开增加时应摊销其开销,并且渐近(在展开中)因子)性能应接近 20 亿次循环。
Both llvm-mca
and iaca
预测展开ntimes 将使循环执行n周期(这将使整个程序执行 20 亿个周期)。
总而言之,问题是:为什么我的循环一旦展开超过 2 次就会慢 25%?