为什么添加汇编注释会在生成的代码中引起如此激进的变化

所以,我有这个代码:

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指令。 我期待着相同的输出,并在其中间的某些评论。 我在这里做一些错误的假设? 海湾合作委员会的优化程序阻碍了asm的评论?

有关优化的交互可以在文档中的“C语言操作数的汇编器指令”页面的一半处进行说明。

海湾合作委员会并没有试图了解任何实际的组装内的asm ; 它唯一知道的内容是你(可选)在输出和input操作数规范和寄存器clobber列表中告诉它的内容。

特别要注意的是:

没有任何输出操作数的asm指令将被视为易失性asm指令。

volatile关键字表示该指令具有重要的副作用[…]

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

请注意,gcc向量化代码,将循环体分成两部分,第一次处理16个项目,第二个处理剩下的部分。

正如Ira所说,编译器不会parsingasm块,所以它不知道它只是一个注释。 即使这样做,也无法知道你的意图。 如果把它们放在每一个中,那么最佳化的循环会使身体翻一番? 你要不要执行1000次? 它不知道,所以它走的安全路线,并回落到简单的单一循环。

我不同意“海湾合作委员会不明白什么是在asm()块”。 例如,gcc可以很好地处理优化参数,甚至重新安排asm()块,以便与生成的C代码混合。 这就是为什么,如果你看看Linux内核的内联汇编程序,它几乎总是以__volatile__为前缀,以确保编译器“不会移动代码”。 我有gcc移动我的“rdtsc”,这使我测量了做某件事情的时间。

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

这并不是说gcc有时候不会被内联汇编程序块混淆,或者只是决定放弃一些特定的优化,因为它不能遵循汇编代码等的后果。更重要的是,它经常会因为丢失clobber标签而感到困惑 – 所以如果你有一些像cpuid这样的改变EAX-EDX值的指令,但是你编写的代码只能使用EAX,编译器可能会把东西存储在EBX,ECX和EDX中,然后当这些寄存器被覆盖时,你的代码会变得非常奇怪。如果你很幸运,它会立即崩溃 – 那么很容易找出发生的事情。 但是,如果你不走运,它会崩溃的路线…另一个棘手的是鸿沟的指令,给edx的第二个结果。 如果你不关心模数,很容易忘记EDX被改变了。

每个汇编注释都是一个断点。 你可以在一个解释器中运行你的程序,这个解释器会在每个注释中断开并打印出每个variables的状态(使用debugging信息)。 这些点必须存在,以便观察环境(寄存器和内存的状态)。

没有评论,就没有观察点存在,循环被编译为一个单一的math函数,考虑一个环境和产生一个修改后的环境。

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

更好的问题是:

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

但是,那么你不会想要读取一个长于海湾合作委员会内部代表性的输出的certificate。