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


82

所以,我有这段代码:

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指令。我期待相同的输出,中间会有一些注释。我在这里做一些错误的假设吗?GCC的优化程序是否受到asm注释的阻碍?


28
我希望GCC(和大多数编译器)将ASM构造像块盒一样对待。因此,他们无法推断出通过这样的盒子会发生什么。这的确抑制了许多优化,尤其是那些跨循环边界进行的优化。
Ira Baxter

10
尝试asm使用空输出和内容清单的扩展形式。
Kerrek SB 2012

4
@ R.MartinhoFernandes :(asm("# im in ur loop" : : );请参阅文档
Mike Seymour

16
请注意,在查看生成的程序集时,可以通过添加-fverbose-asm标志获得更多帮助,该标志添加一些注释以帮助标识事物在寄存器之间的移动方式。
Matthew Slattery 2012年

1
很有意思。可以用来有选择地避免循环优化吗?
SChepurin 2012年

Answers:


62

在文档的“带有C表达式操作数的汇编程序指令”页面的大约一半处,说明了与优化的交互。

GCC不会试图了解里面的任何实际组件asm; 它唯一了解的内容是您(可选)在输出和输入操作数规范以及寄存器清除列表中告诉它的内容。

特别注意:

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

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

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


1
请注意,基本Asm语句的副作用不得包括修改寄存器或C ++代码曾经读/写的任何内存。但是是的,该asm语句必须在C ++抽象机中每次都运行一次,并且GCC选择不进行矢量化,然后每行连续16次发出asm paddb。我认为这是合法的,因为char访问不是volatile。(与扩展的带有声明的asm声明不同"memory"
Peter Cordes

1
请参阅gcc.gnu.org/wiki/ConvertBasicAsmToExtended,以了解通常不使用GNU C Basic Asm语句的原因。尽管此用例(仅是注释标记)是少数几种不需尝试的用例之一。
Peter Cordes

23

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

正如Ira所说,编译器不会解析asm块,因此它不知道这只是一个注释。即使这样做,也无法知道您的意图。最佳化的循环使主体增加了一倍,是否应该将其放入每个循环中?您是否希望它不执行1000次?它不知道,所以它走了一条安全路线,又退回到了简单的单循环。


3

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

如记录所示,gcc将某些类型的asm()块视为“特殊”块,因此不会在块的任一侧优化代码。

这并不是说gcc有时不会被内联汇编程序块弄糊涂,或者只是因为无法跟踪汇编程序代码的结果等而决定放弃某些特定的优化。更重要的是,它经常会因缺少clobber标签而感到困惑-因此,如果您有类似的说明,cpuid改变了EAX-EDX的值,但是您编写了代码,使其仅使用EAX,编译器可能会将它们存储在EBX,ECX和EDX中,然后当这些寄存器被覆盖时,您的代码表现得很奇怪。您很幸运,它立即崩溃了-然后很容易弄清楚发生了什么。但是,如果您不走运,它会严重崩溃……另一个棘手的问题是除法指令,该指令在edx中给出第二个结果。如果您不关心模数,则很容易忘记EDX已更改。


1
gcc确实不了解asm块中的内容-您必须通过扩展的asm语句告诉它。没有这些额外的信息,gcc将不会在这些块周围移动。在您陈述的情况下,gcc也不会感到困惑-您只是通过告诉gcc实际上可以使用这些寄存器,而您的代码却破坏了它们,从而使您犯了编程错误。
记得莫妮卡

回复较晚,但我认为值得一提。volatile asm告诉GCC该代码可能具有“重要的副作用”,并且会格外小心地处理它。它可能仍会作为死代码优化的一部分被删除或移出。与C代码的交互需要假设这种情况(罕见)并施加严格的顺序评估(例如,通过在asm中创建依赖项)。
edmz

GNU C基本asm(没有操作数约束,如OP一样asm(""))是隐式易失的,就像没有输出操作数的扩展asm一样。GCC不能理解asm模板字符串,而只能理解约束。这就是为什么使用约束向编译器准确完整地描述您的asm至关重要。将操作数替换为模板字符串不需要比printf使用格式字符串更多的了解。TL:DR:除了使用纯注释之类的用例外,请勿将GNU C Basic asm用于任何事情。
Peter Cordes

-2

现在,此答案已被修改:它最初的想法是将内联的基本Asm作为一个非常明确地指定的工具,但是与GCC中的工具不同。基本汇编记忆很弱,因此答案已被编辑。

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

编辑:但是,当您使用基本Asm时,它就坏了。内联asmasm函数主体中的语句)没有显式的内容清单是GCC中的一个弱指定功能,很难定义其行为。似乎并没有(特别是我没有完全理解其保证)附加到任何东西,因此,如果该函数运行,必须在某个点运行汇编代码,但不清楚何时为任何非代码运行汇编代码琐碎的优化水平。可以与相邻指令重新排序的断点不是一个非常有用的“断点”。结束编辑

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

没有注释,就不存在观察点,并且将循环编译为带有环境并产生修改后环境的单个数学函数。

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

更好的问题是:

您好,GCC。为什么您认为此asm输出正在实现源代码?请以各种假设逐步说明。

但是那样的话,您将不需要阅读比用GCC内部表示形式编写的asm输出更长的证明。


1
这些点必须存在,以便您观察环境(寄存器和内存的状态)。-对于未优化的代码,可能是这样。启用优化后,整个功能可能会从二进制文件中消失。我们在这里谈论优化的代码。
Bartek Banachewicz

1
我们正在谈论的是在启用优化的情况下编译生成的程序集。因此,您错误地指出任何事物都必须存在。
Bartek Banachewicz 2015年

1
是的,IDK为什么会有人愿意,并同意没有人应该这样做。正如我在上一条评论中的链接所解释的那样,没有人应该这样做,并且一直在争论如何加强它(例如使用隐式"memory"Clobber)作为确实存在的现有越野车代码的创可贴。即使对于这样的指令asm("cli")仅影响编译器生成的代码不会触及的部分架构状态,您仍然需要将其排序。编译器生成的加载/存储(例如,如果要禁用关键部分周围的中断)。
Peter Cordes

1
由于不安全地破坏红色区域,因此,除非您add rsp, -128先这样做,否则即使asm语句内部效率不高的手动保存/恢复寄存器(使用push / pop)也不安全。但是这样做显然是死了。
Peter Cordes

1
当前,GCC对待Basic Asm完全等效asm("" :::)(隐含易失性,因为它没有输出,但不受输入或输出依赖关系与其余代码的束缚"memory"。当然,它不会%operand对模板字符串进行替换,因此文字%不必以进行转义%%。因此,是的,同意,在__attribute__((naked))功能和全局范围之外弃用Basic Asm是一个好主意。
Peter Cordes
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.