为什么添加内联程序集注释会导致 GCC 生成的代码发生如此根本的变化?
Why does adding inline assembly comments cause such radical change in GCC's generated code?
所以,我有这样的代码:
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内部表示编写的。
- C++我的数学有什么问题,为什么我的代码不能正确循环
- 代码在main()中运行,但在函数中出现错误
- 在VS代码中交叉编译Windows与Linux上的MinGW的SDL程序
- 编译包含字符串的代码时遇到问题
- 我在c++代码中生成了一个运行时#3异常
- 如何在linux终端中同时编译和运行c++代码
- 为cl.exe(Visual Studio代码)指定命令行C++版本
- 在Linux for Windows上编译C++代码时出错
- 我的字符计数代码计算错误.为什么
- 孤立代码块在结构中引发异常
- C++11:具有互斥锁的线程看到原子变量的值发生变化,尽管这是唯一可以改变它的代码
- EM_ASM里面的 JS 代码会发生什么变化?
- 为什么认为参考文献在C 底漆第5个中没有变化,以下代码有效
- 从旧的 C 样式指针移动到C++智能指针,代码几乎没有变化
- 为什么添加内联程序集注释会导致 GCC 生成的代码发生如此根本的变化?
- 一个从未执行过的代码中的一个小变化怎么会有什么不同呢
- 子类中的这种变化是否需要重新编译依赖于超类的代码?
- 为什么代码中 get<>() 赋值会在构造之外发生变化?
- 加速度计"高度变化"代码 PIC
- 为什么以下代码中"C"的值会发生变化?