为什么添加内联程序集注释会导致 GCC 生成的代码发生如此剧烈的变化?

所以,我有这个代码:

constexpr unsigned N = 1000;
void f1(char* sum, char* a, char* b) {
for(int i = 0; i < N; ++i) {
sum[i] = a[i] + b[i];
}
}


void f2(char* sum, char* a, char* b) {
char* end = sum + N;
while(sum != end) {
*sum++ = *a++ + *b++;
}
}

我想看看 GCC 4.7.2生成的代码,所以我运行了 g++ -march=native -O3 -masm=intel -S a.c++ -std=c++11,得到了以下输出:

        .file   "a.c++"
.intel_syntax noprefix
.text
.p2align 4,,15
.globl  _Z2f1PcS_S_
.type   _Z2f1PcS_S_, @function
_Z2f1PcS_S_:
.LFB0:
.cfi_startproc
lea     rcx, [rdx+16]
lea     rax, [rdi+16]
cmp     rdi, rcx
setae   r8b
cmp     rdx, rax
setae   cl
or      cl, r8b
je      .L5
lea     rcx, [rsi+16]
cmp     rdi, rcx
setae   cl
cmp     rsi, rax
setae   al
or      cl, al
je      .L5
xor     eax, eax
.p2align 4,,10
.p2align 3
.L3:
movdqu  xmm0, XMMWORD PTR [rdx+rax]
movdqu  xmm1, XMMWORD PTR [rsi+rax]
paddb   xmm0, xmm1
movdqu  XMMWORD PTR [rdi+rax], xmm0
add     rax, 16
cmp     rax, 992
jne     .L3
mov     ax, 8
mov     r9d, 992
.L2:
sub     eax, 1
lea     rcx, [rdx+r9]
add     rdi, r9
lea     r8, [rax+1]
add     rsi, r9
xor     eax, eax
.p2align 4,,10
.p2align 3
.L4:
movzx   edx, BYTE PTR [rcx+rax]
add     dl, BYTE PTR [rsi+rax]
mov     BYTE PTR [rdi+rax], dl
add     rax, 1
cmp     rax, r8
jne     .L4
rep
ret
.L5:
mov     eax, 1000
xor     r9d, r9d
jmp     .L2
.cfi_endproc
.LFE0:
.size   _Z2f1PcS_S_, .-_Z2f1PcS_S_
.p2align 4,,15
.globl  _Z2f2PcS_S_
.type   _Z2f2PcS_S_, @function
_Z2f2PcS_S_:
.LFB1:
.cfi_startproc
lea     rcx, [rdx+16]
lea     rax, [rdi+16]
cmp     rdi, rcx
setae   r8b
cmp     rdx, rax
setae   cl
or      cl, r8b
je      .L19
lea     rcx, [rsi+16]
cmp     rdi, rcx
setae   cl
cmp     rsi, rax
setae   al
or      cl, al
je      .L19
xor     eax, eax
.p2align 4,,10
.p2align 3
.L17:
movdqu  xmm0, XMMWORD PTR [rdx+rax]
movdqu  xmm1, XMMWORD PTR [rsi+rax]
paddb   xmm0, xmm1
movdqu  XMMWORD PTR [rdi+rax], xmm0
add     rax, 16
cmp     rax, 992
jne     .L17
add     rdi, 992
add     rsi, 992
add     rdx, 992
mov     r8d, 8
.L16:
xor     eax, eax
.p2align 4,,10
.p2align 3
.L18:
movzx   ecx, BYTE PTR [rdx+rax]
add     cl, BYTE PTR [rsi+rax]
mov     BYTE PTR [rdi+rax], cl
add     rax, 1
cmp     rax, r8
jne     .L18
rep
ret
.L19:
mov     r8d, 1000
jmp     .L16
.cfi_endproc
.LFE1:
.size   _Z2f2PcS_S_, .-_Z2f2PcS_S_
.ident  "GCC: (GNU) 4.7.2"
.section        .note.GNU-stack,"",@progbits

我不擅长阅读汇编,所以我决定添加一些标记来知道循环的主体去了哪里:

constexpr unsigned N = 1000;
void f1(char* sum, char* a, char* b) {
for(int i = 0; i < N; ++i) {
asm("# im in ur loop");
sum[i] = a[i] + b[i];
}
}


void f2(char* sum, char* a, char* b) {
char* end = sum + N;
while(sum != end) {
asm("# im in ur loop");
*sum++ = *a++ + *b++;
}
}

海湾合作委员会指出:

    .file   "a.c++"
.intel_syntax noprefix
.text
.p2align 4,,15
.globl  _Z2f1PcS_S_
.type   _Z2f1PcS_S_, @function
_Z2f1PcS_S_:
.LFB0:
.cfi_startproc
xor eax, eax
.p2align 4,,10
.p2align 3
.L2:
#APP
# 4 "a.c++" 1
# im in ur loop
# 0 "" 2
#NO_APP
movzx   ecx, BYTE PTR [rdx+rax]
add cl, BYTE PTR [rsi+rax]
mov BYTE PTR [rdi+rax], cl
add rax, 1
cmp rax, 1000
jne .L2
rep
ret
.cfi_endproc
.LFE0:
.size   _Z2f1PcS_S_, .-_Z2f1PcS_S_
.p2align 4,,15
.globl  _Z2f2PcS_S_
.type   _Z2f2PcS_S_, @function
_Z2f2PcS_S_:
.LFB1:
.cfi_startproc
xor eax, eax
.p2align 4,,10
.p2align 3
.L6:
#APP
# 12 "a.c++" 1
# im in ur loop
# 0 "" 2
#NO_APP
movzx   ecx, BYTE PTR [rdx+rax]
add cl, BYTE PTR [rsi+rax]
mov BYTE PTR [rdi+rax], cl
add rax, 1
cmp rax, 1000
jne .L6
rep
ret
.cfi_endproc
.LFE1:
.size   _Z2f2PcS_S_, .-_Z2f2PcS_S_
.ident  "GCC: (GNU) 4.7.2"
.section    .note.GNU-stack,"",@progbits

这是相当短的,并有一些重大的差异,如缺乏 SIMD 指令。我期待的是同样的输出,中间还有一些注释。我是不是做了错误的假设?海湾合作委员会的优化器是否受到影响?

8307 次浏览

注意,gcc 向量化了代码,将循环体分成两部分,第一部分一次处理16个项,第二部分稍后处理其余部分。

正如 Ira 注释的那样,编译器不解析 asm 块,所以它不知道它只是一个注释。就算它知道,它也无法知道你的意图。经过优化的循环使身体翻倍,它应该把你的高潮放在每一个吗?你希望它不被执行1000次吗?它不知道,所以它走了安全的路线,回到了简单的单循环。

与优化的交互在文档的 “具有 C 表达式操作数的汇编程序指令”页面的一半左右被解释。

GCC 不会尝试理解 asm中的任何实际程序集; 它对内容的唯一了解是您(可选地)在输出和输入操作数规范以及寄存器清除器列表中告诉它的内容。

特别要注意:

没有任何输出操作数的 asm指令将被等同于易失性 asm指令。

还有

volatile关键字表明指令有重要的副作用[ ... ]

所以 asm在你的循环中的存在抑制了向量优化,因为 GCC 认为它有副作用。

我不同意“ gcc 不理解 asm()块中的内容”。例如,gcc 可以很好地处理参数优化,甚至可以重新排列 asm()块,使其与生成的 C 代码混合在一起。这就是为什么,如果你看看内联汇编,比如 Linux 内核,它几乎总是以 __volatile__作为前缀,以确保编译器“不会移动代码”。我已经让 gcc 移动了我的“ rdtsc”,这样我就可以测量完成某些事情所需要的时间。

如文档所述,gcc 将某些类型的 asm()块视为“特殊”,因此不会优化块的任何一边的代码。

这并不是说 gcc 有时不会被内联汇编块搞糊涂,或者干脆决定放弃某些特定的优化,因为它不能遵循汇编代码的结果,等等。更重要的是,它经常会被丢失的重锤标记搞糊涂——所以如果你有一些指令,比如 cpuid,它会改变 EAX-EDX 的值,但是你编写的代码只使用 EAX,编译器可能会在 EBX,ECX 和 EDX 中存储东西,然后当这些寄存器被覆盖时,你的代码会表现得非常奇怪... ... 如果你幸运的话,它会立即崩溃——那么很容易找出发生了什么。但是如果你运气不好,它会一直崩溃下去... ... 另一个棘手的问题是在 edx 中给出第二个结果的除法指令。如果你不关心模量,很容易忘记 EDX 是改变了。

这个答案现在被修改了: 它最初是基于一种思维模式编写的,考虑到内联 Basic Asm 作为一个非常强烈指定的工具,但它与 GCC 中的工具完全不同。基本的反肢动作很弱,所以答案被编辑了。

每个程序集注释充当断点。

编辑: 但一个破碎的,因为你使用基本的 Asm。内联 asm(函数体内的 asm语句)没有显式的修补列表是 GCC 中一个弱指定的特性,它的行为很难定义。它没有将 看起来(我没有完全掌握它的保证)附加到任何特定的东西上,所以如果函数运行的话,汇编代码必须在某个时候运行,那么就是 它不清楚什么时候运行任何非平凡的优化级别。可以用邻接指令重新排序的断点不是一个非常有用的“断点”。结束编辑

您可以在解释器中运行程序,该解释器在每条注释处中断,并打印出每个变量的状态(使用调试信息)。这些点必须存在,以便您观察环境(寄存器和内存的状态)。

如果没有注释,就不存在观察点,并且循环被编译为一个单一的数学函数,接受一个环境并生成一个修改过的环境。

你想知道一个无意义问题的答案: 你想知道每个指令(或者块,或者指令的范围)是如何编译的,但是没有单独的指令(或者块)被编译; 整个东西是作为一个整体被编译的。

一个更好的问题是:

你好,海湾合作委员会。你为什么认为这个输出实现了源代码?请用每一个假设一步一步地解释。

但是你不会想要阅读一个证明,长于高斯输出,写在 GCC 的内部表示。