所以,我有这个代码:
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评论的阻碍?
答案 0 :(得分:58)
有关优化的互动在文档中"Assembler Instructions with C Expression Operands"页面的中间位置进行了解释。
GCC不会试图了解asm
内的任何实际装配;它对内容的唯一了解就是你(可选)在输出和输入操作数规范以及寄存器clobber列表中告诉它。
特别注意:
没有任何输出操作数的
asm
指令将被视为与易失性asm
指令完全相同。
和
volatile
关键字表示该指令具有重要的副作用[...]
因此,循环中asm
的存在抑制了矢量化优化,因为GCC认为它有副作用。
答案 1 :(得分:21)
请注意,gcc对代码进行了矢量化,将循环体分成两部分,第一部分一次处理16个项目,第二部分稍后处理剩余部分。
正如Ira评论的那样,编译器不会解析asm块,所以它不知道它只是一个注释。即使这样,它也无法知道你的意图。优化的循环使身体加倍,是否应该将你的asm放在每个?你想它不执行1000次吗?它不知道,所以它走向安全路线并回到简单的单循环。
答案 2 :(得分:4)
我不同意&#34; gcc不了解asm()
块&#34;中的内容。例如,gcc可以很好地处理优化参数,甚至可以重新安排asm()
块,使其与生成的C代码混合在一起。这就是为什么,如果你看一下Linux内核中的内联汇编程序,它几乎总是以__volatile__
为前缀,以确保编译器不会移动代码&#34;。我有gcc移动我的&#34; rdtsc&#34;周围,这使我测量了做某件事所花费的时间。
如文档所述,gcc将某些类型的asm()
块视为&#34; special&#34;,因此不会优化块的任何一侧的代码。
这并不是说gcc赢了,有时会被内联汇编程序块搞糊涂,或者只是决定放弃某些特定的优化,因为它无法跟上汇编程序代码等等。更重要的是,它常常会因为缺少clobber标记而感到困惑 - 所以如果你有一些像cpuid
这样的指令可以改变EAX-EDX的值,那么你可以编写代码以便它只使用EAX,编译器可能会将内容存储在EBX,ECX和EDX中,然后当这些寄存器被覆盖时,你的代码就会非常奇怪...如果你很幸运,它会立即崩溃 - 那么它很容易弄明白怎么回事。但是,如果你运气不好,它就会崩溃了......另一个棘手的问题是在edx中给出第二个结果的除法指令。如果你不关心模数,很容易忘记EDX被改变了。
答案 3 :(得分:-2)
每个程序集注释都充当断点。您可以在解释器中运行程序,该解释器会在每个注释中断,并打印出每个变量的状态(使用调试信息)。这些点必须存在,以便您观察环境(寄存器和存储器的状态)。
没有评论,就没有观察点,循环被编译为一个环境并产生修改环境的单一数学函数。
你想知道一个毫无意义的问题的答案:你想知道每个指令(或者可能是块,或者可能是指令范围)是如何编译的,但是没有编译单个隔离指令(或块);整个东西都编译成一个整体。
更好的问题是:
你好GCC。为什么你认为这个asm输出正在实现源代码?请按照每个假设逐步解释。
但是那时你不会想要读取比asm输出更长的证明,用GCC内部表示法来写。