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

Why does adding inline assembly comments cause such radical change in GCC's generated code?

本文关键字:代码 变化 添加 注释 程序集 为什么 GCC      更新时间:2023-10-16

所以,我有这样的代码:

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++;
}
}

GCC吐出了这个:

.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指令。我期待着同样的输出,中间有一些评论。我是不是在做一些错误的假设?GCC的优化器是否受到asm注释的阻碍?

与优化的交互在文档中的"带有C表达式操作数的汇编指令"页面的一半处进行了解释。

GCC不试图理解asm内部的任何实际程序集;它唯一知道的内容是您(可选)在输出和输入操作数规范以及寄存器阻塞器列表中告诉它的内容。

特别注意:

没有任何输出操作数的asm指令将与易失性asm指令相同对待。

volatile关键字表示指令具有重要的副作用〔…〕

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

请注意,gcc对代码进行了矢量化,将循环体分为两部分,第一部分一次处理16个项目,第二部分稍后处理其余部分。

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

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

如文档所示,gcc将某些类型的asm()块视为"特殊",因此不会优化块两侧的代码。

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

这个答案现在被修改了:它最初是以一种将内联Basic Asm视为一个非常强的指定工具的心态编写的,但它与GCC中的完全不同。基本的Asm很弱,所以答案被编辑了

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

编辑:但一个坏的,因为你使用基本阿斯姆。没有显式clobber列表的内联asm(函数体内的asm语句)是GCC中的一个弱指定特性,其行为很难定义。它似乎没有(我没有完全掌握它的保证)附加到任何特定的东西上,所以虽然如果运行函数,程序集代码必须在某个时候运行,但不清楚它何时在任何非平凡的优化级别上运行。可以与相邻指令一起重新排序的断点不是很有用的"断点"。结束编辑

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

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

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

一个更好的问题是:

你好,GCC。为什么你认为这个asm输出实现了源代码?请一步一步地解释,每一个假设

但是,您不希望阅读比asm输出更长的证明,asm输出是根据GCC内部表示编写的。