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

83

好的,我有这段代码:

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优化器受到了阻碍?


30
我预期GCC(以及大多数编译器)会像处理代码块一样处理汇编语句。因此,它们无法推断出这样的语句内部发生了什么。这限制了许多优化的实现,尤其是那些跨越循环边界的优化。 - Ira Baxter
10
尝试使用扩展的asm形式,其中输出和破坏列表为空。 - Kerrek SB
4
@R.MartinhoFernandes: asm("# im in ur loop" : : ); 可以翻译为:汇编语句 asm("# im in ur loop" : : );,意思是“我在你的循环里面”,更多信息请参考文档 - Mike Seymour
17
请注意,当查看生成的汇编代码时,您可以通过添加“-fverbose-asm”标志来获得更多帮助,该标志将添加一些注释,以帮助识别寄存器之间的移动方式。 - Matthew Slattery
1
非常有趣。可以用于循环中有选择地避免优化吗? - SChepurin
显示剩余4条评论
4个回答

66

《使用C表达式操作数的汇编指令》页面中间部分有关于优化交互的解释。

GCC不会理解asm内部的任何汇编内容;它所知道的关于内容的信息只有你在输出和输入操作数规范以及寄存器摧毁列表中(可选)告诉它的。

特别地,请注意:

没有任何输出操作数的asm指令将与具有volatileasm指令完全相同。

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

因此,在您的循环内部存在asm将抑制矢量化优化,因为GCC假定它具有副作用。


1
请注意,Basic Asm语句的副作用不得包括修改寄存器或任何C++代码读取/写入的内存。但是,是的,asm语句必须在C++抽象机中每次运行一次,并且GCC选择不对其进行矢量化,然后连续16次发出asm以进行paddb。我认为这是合法的,因为char访问不是volatile的。(与具有“memory”破坏的扩展asm语句不同) - Peter Cordes
1
请参阅https://gcc.gnu.org/wiki/ConvertBasicAsmToExtended,了解一般情况下不使用GNU C基本汇编语句的原因。尽管这种用例(仅作为注释标记)是少数几个可以尝试的情况之一。 - Peter Cordes

23
请注意,gcc将代码向量化,将循环体分成两部分,第一部分一次处理16个项目,第二部分稍后处理剩余的项目。
正如Ira所评论的那样,编译器不解析asm块,因此它不知道它只是一个注释。即使它知道,也没有办法知道您的意图。优化的循环体翻倍,应该在每个循环中都放置您的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可以使用那些寄存器来破坏它们的代码而犯了一个编程错误。 - Remember Monica
晚回复,但我认为值得一说。volatile asm 告诉 GCC 代码可能具有“重要的副作用”,它会更加特别地处理它。它仍然可能被删除作为死代码优化的一部分或移出。与 C 代码的交互需要假定这种(罕见)情况并强制执行严格的顺序评估(例如通过在 asm 中创建依赖项)。 - edmz
GNU C基本asm(没有操作数约束,就像OP的asm("")一样)是隐含易失性的,就像没有输出操作数的Extended asm一样。 GCC不理解asm模板字符串,只理解其约束条件。这就是为什么使用约束条件准确完整地描述您的asm对于编译器至关重要的原因。将操作数替换为模板字符串不需要比printf使用格式字符串更多的理解。简而言之,除了像这样纯注释的用例,不要在任何情况下使用GNU C基本asm。 - Peter Cordes

-2

这个答案现在已经修改:最初编写时考虑了内联基本汇编作为一个相当强烈规定的工具,但在GCC中并不是这样。基本汇编是弱的,因此答案被编辑过。

每个汇编注释都充当断点。

编辑:但是,由于使用了Basic Asm,所以它是一个损坏的断点。在GCC中,没有显式clobber列表的内联asm(函数体内的asm语句)是一个弱规定的特性,其行为很难定义。它似乎没有与任何特定内容相关联(我不完全理解它的保证),因此,如果运行函数,则必须在某个时刻运行汇编代码,但对于任何非平凡优化级别,不清楚何时运行。可以重新排序与相邻指令的断点不是一个非常有用的“断点”。结束编辑

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

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

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

一个更好的问题是:

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

但是你不会想读一个比汇编输出还长的证明,用GCC内部表示来写。


1
这些点必须存在,以便您观察环境(寄存器和内存状态)。这可能适用于未经优化的代码。启用优化后,整个函数可能会从二进制文件中消失。我们在这里谈论的是优化后的代码。 - Bartek Banachewicz
1
我们正在讨论启用优化编译后生成的汇编代码。因此,您错误地声明任何东西必须存在。 - Bartek Banachewicz
1
是的,我不知道为什么有人会这样做,也同意没有人应该这样做。正如我上一条评论中的链接所解释的那样,没有人应该这样做,并且已经有关于加强它的争论(例如使用隐式“memory”破坏器作为现有错误代码的临时解决方案)。即使对于像asm(“cli”)这样只影响编译器生成的代码不涉及的架构状态的指令,您仍然需要按顺序处理它们。与编译器生成的加载/存储(例如,如果您在关键部分周围禁用中断)相关。 - Peter Cordes
1
由于清除红区不安全,即使在 asm 语句中进行低效的手动寄存器保存/恢复(使用 push/pop)也是不安全的,除非您首先执行 add rsp, -128。但这样做显然是愚蠢的。 - Peter Cordes
1
目前GCC将Basic Asm视为与asm("" :::)完全等效(隐式易失性,因为它没有输出,但不受输入或输出依赖关系的限制。当然,也没有“memory”破坏)。 当然,在模板字符串上不执行%操作数替换,因此文字%不必转义为%%。 因此,是的,同意,在__attribute __((naked))函数和全局范围之外废弃Basic Asm是一个好主意。 - Peter Cordes
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接