【问题标题】:Why does adding inline assembly comments cause such radical change in GCC's generated code?为什么添加内联汇编注释会导致 GCC 生成的代码发生如此巨大的变化?
【发布时间】:2012-12-06 23:15:34
【问题描述】:

所以,我有这个代码:

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

【问题讨论】:

  • 我希望 GCC(和大多数编译器)将 ASM 构造视为块框。所以他们无法推断通过这样一个盒子会发生什么。这确实抑制了许多优化,尤其是那些跨越循环边界的优化。
  • 尝试使用空输出和破坏列表的扩展 asm 表单。
  • @R.MartinhoFernandes: asm("# im in ur loop" : : );(见documentation
  • 请注意,在查看生成的程序集时,您可以通过添加 -fverbose-asm 标志获得更多帮助,该标志会添加一些注释来帮助识别寄存器之间的移动方式。
  • 非常有趣。可用于选择性地避免循环中的优化?

标签: c++ gcc assembly optimization inline-assembly


【解决方案1】:

文档中"Assembler Instructions with C Expression Operands" 页面的中途解释了与优化的交互。

GCC 不会尝试理解asm 中的任何实际程序集;它唯一知道的内容是您(可选地)在输出和输入操作数规范以及寄存器破坏列表中告诉它的内容。

特别注意:

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

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

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

【讨论】:

  • 请注意,Basic Asm 语句的副作用不得包括修改寄存器或您的 C++ 代码曾经读/写的任何内存。但是,是的,asm 语句必须每次在 C++ 抽象机中运行一次,并且 GCC 选择不进行矢量化,然后根据paddb 连续发出 16 次 asm。我认为那是合法的,因为字符访问不是volatile。 (与带有 "memory" clobber 的扩展 asm 语句不同)
  • 请参阅gcc.gnu.org/wiki/ConvertBasicAsmToExtended,了解一般不使用 GNU C Basic Asm 语句的原因。虽然这个用例(只是一个评论标记)是少数几个可以尝试的案例之一。
【解决方案2】:

请注意,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 已更改。

    【讨论】:

    • gcc 真的不了解 asm 块中的内容 - 您必须通过扩展的 asm 语句告诉它。如果没有这些额外信息,gcc 将不会在这些块周围移动。 gcc 在您所说的情况下也不会感到困惑-您只是通过告诉 gcc 它可以使用这些寄存器而犯了编程错误,而实际上,您的代码破坏了它们。
    • 回复晚了,但我觉得值得一说。 volatile asm 告诉 GCC 代码可能有“重要的副作用”,它会更加小心地处理它。它可能仍然作为死代码优化的一部分被删除或移出。与 C 代码的交互需要假设这种(罕见的)情况并强制执行严格的顺序评估(例如,通过在 asm 中创建依赖项)。
    • GNU C Basic asm(无操作数约束,如 OP 的 asm(""))是隐式易失的,就像没有输出操作数的扩展 asm。 GCC 不理解 asm 模板字符串,只理解约束;这就是为什么使用约束向编译器准确、完整地描述您的 asm 是必要的。将操作数代入模板字符串并不比使用格式字符串的printf 更容易理解。 TL:DR: 不要将 GNU C Basic asm 用于任何事情,除了像这样的纯 cmets 用例。
    【解决方案4】:

    这个答案现在被修改了:它最初是用一种思维方式编写的,考虑到内联 Basic Asm 是一个非常明确的工具,但它与 GCC 中的完全不同。 Basic Asm 很弱,因此编辑了答案。

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

    编辑:但是一个损坏的,因为您使用基本 Asm。内联 asm(函数体内的 asm 语句)没有明确的 clobber 列表是 GCC 中的一个弱指定特性,它的行为很难定义。它没有似乎(我不完全掌握它的保证)附加到任何特别的东西上,所以虽然如果函数运行,汇编代码必须在某个时候运行,它不是'不清楚何时运行任何非平凡的优化级别。可以用相邻指令重新排序的断点不是一个非常有用的“断点”。结束编辑

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

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

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

    一个更好的问题是:

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

    但是,您不会希望阅读比 asm 输出更长的证明,以 GCC 内部表示形式编写。

    【讨论】:

    • 这些点必须存在,这样您才能观察环境(寄存器和内存的状态)。 - 这可能适用于未优化的代码。启用优化后,整个函数可能会从二进制文件中消失。我们在这里讨论的是优化代码。
    • 我们谈论的是在启用优化的情况下编译生成的程序集。因此,您说任何事物都必须存在是错误
    • 是的,我知道为什么有人会这样做,并且同意没有人应该这样做。正如我上一条评论中的链接所解释的那样,没有人应该这样做,并且一直存在关于加强它的争论(例如,使用隐含的"memory" clobber)作为肯定存在的现有错误代码的创可贴。即使对于像asm("cli") 这样只影响编译器生成的代码不涉及的部分架构状态的指令,您仍然需要对其进行排序。编译器生成的加载/存储(例如,如果您在关键部分周围禁用中断)。
    • 由于破坏红色区域并不安全,因此即使在 asm 语句中手动保存/恢复寄存器(使用 push/pop)效率低下也不安全,除非您先 add rsp, -128。但这样做显然是脑残。
    • 当前 GCC 将 Basic Asm 视为与 asm("" :::) 完全相同(隐含 volatile,因为它没有输出,但不通过输入或输出依赖关系与其余代码绑定。并且没有 "memory" clobber) .当然,它不会对模板字符串进行%operand 替换,因此不必将文字% 转义为%%。所以是的,同意,在 __attribute__((naked)) 函数和全局范围之外弃用基本 Asm 是个好主意。
    猜你喜欢
    • 2012-09-02
    • 2021-06-13
    • 1970-01-01
    • 2019-12-28
    • 2022-01-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-05-08
    相关资源
    最近更新 更多