如何从 GCC/clang 程序集输出中删除“噪音”?

新手上路,请多包涵

我想检查在我的代码中应用 boost::variant 的程序集输出,以查看哪些中间调用被优化掉了。

当我编译以下示例时(在 GCC 5.3 中使用 g++ -O3 -std=c++14 -S ),编译器似乎优化了所有内容并直接返回 100:

 (...)
main:
.LFB9320:
    .cfi_startproc
    movl    $100, %eax
    ret
    .cfi_endproc
(...)


 #include <boost/variant.hpp>

struct Foo
{
    int get() { return 100; }
};

struct Bar
{
    int get() { return 999; }
};

using Variant = boost::variant<Foo, Bar>;

int run(Variant v)
{
    return boost::apply_visitor([](auto& x){return x.get();}, v);
}
int main()
{
    Foo f;
    return run(f);
}

但是,完整的汇编输出包含的内容远不止上面的摘录,在我看来,它从未被调用过。 有没有办法告诉 GCC/clang 删除所有“噪音”,并在程序运行时输出实际调用的内容?


完整的汇编输出:

     .file   "main1.cpp"
    .section    .rodata.str1.8,"aMS",@progbits,1
    .align 8
.LC0:
    .string "/opt/boost/include/boost/variant/detail/forced_return.hpp"
    .section    .rodata.str1.1,"aMS",@progbits,1
.LC1:
    .string "false"
    .section    .text.unlikely._ZN5boost6detail7variant13forced_returnIvEET_v,"axG",@progbits,_ZN5boost6detail7variant13forced_returnIvEET_v,comdat
.LCOLDB2:
    .section    .text._ZN5boost6detail7variant13forced_returnIvEET_v,"axG",@progbits,_ZN5boost6detail7variant13forced_returnIvEET_v,comdat
.LHOTB2:
    .p2align 4,,15
    .weak   _ZN5boost6detail7variant13forced_returnIvEET_v
    .type   _ZN5boost6detail7variant13forced_returnIvEET_v, @function
_ZN5boost6detail7variant13forced_returnIvEET_v:
.LFB1197:
    .cfi_startproc
    subq    $8, %rsp
    .cfi_def_cfa_offset 16
    movl    $_ZZN5boost6detail7variant13forced_returnIvEET_vE19__PRETTY_FUNCTION__, %ecx
    movl    $49, %edx
    movl    $.LC0, %esi
    movl    $.LC1, %edi
    call    __assert_fail
    .cfi_endproc
.LFE1197:
    .size   _ZN5boost6detail7variant13forced_returnIvEET_v, .-_ZN5boost6detail7variant13forced_returnIvEET_v
    .section    .text.unlikely._ZN5boost6detail7variant13forced_returnIvEET_v,"axG",@progbits,_ZN5boost6detail7variant13forced_returnIvEET_v,comdat
.LCOLDE2:
    .section    .text._ZN5boost6detail7variant13forced_returnIvEET_v,"axG",@progbits,_ZN5boost6detail7variant13forced_returnIvEET_v,comdat
.LHOTE2:
    .section    .text.unlikely._ZN5boost6detail7variant13forced_returnIiEET_v,"axG",@progbits,_ZN5boost6detail7variant13forced_returnIiEET_v,comdat
.LCOLDB3:
    .section    .text._ZN5boost6detail7variant13forced_returnIiEET_v,"axG",@progbits,_ZN5boost6detail7variant13forced_returnIiEET_v,comdat
.LHOTB3:
    .p2align 4,,15
    .weak   _ZN5boost6detail7variant13forced_returnIiEET_v
    .type   _ZN5boost6detail7variant13forced_returnIiEET_v, @function
_ZN5boost6detail7variant13forced_returnIiEET_v:
.LFB9757:
    .cfi_startproc
    subq    $8, %rsp
    .cfi_def_cfa_offset 16
    movl    $_ZZN5boost6detail7variant13forced_returnIiEET_vE19__PRETTY_FUNCTION__, %ecx
    movl    $39, %edx
    movl    $.LC0, %esi
    movl    $.LC1, %edi
    call    __assert_fail
    .cfi_endproc
.LFE9757:
    .size   _ZN5boost6detail7variant13forced_returnIiEET_v, .-_ZN5boost6detail7variant13forced_returnIiEET_v
    .section    .text.unlikely._ZN5boost6detail7variant13forced_returnIiEET_v,"axG",@progbits,_ZN5boost6detail7variant13forced_returnIiEET_v,comdat
.LCOLDE3:
    .section    .text._ZN5boost6detail7variant13forced_returnIiEET_v,"axG",@progbits,_ZN5boost6detail7variant13forced_returnIiEET_v,comdat
.LHOTE3:
    .section    .text.unlikely,"ax",@progbits
.LCOLDB4:
    .text
.LHOTB4:
    .p2align 4,,15
    .globl  _Z3runN5boost7variantI3FooJ3BarEEE
    .type   _Z3runN5boost7variantI3FooJ3BarEEE, @function
_Z3runN5boost7variantI3FooJ3BarEEE:
.LFB9310:
    .cfi_startproc
    subq    $8, %rsp
    .cfi_def_cfa_offset 16
    movl    (%rdi), %eax
    cltd
    xorl    %edx, %eax
    cmpl    $19, %eax
    ja  .L7
    jmp *.L9(,%rax,8)
    .section    .rodata
    .align 8
    .align 4
.L9:
    .quad   .L30
    .quad   .L10
    .quad   .L7
    .quad   .L7
    .quad   .L7
    .quad   .L7
    .quad   .L7
    .quad   .L7
    .quad   .L7
    .quad   .L7
    .quad   .L7
    .quad   .L7
    .quad   .L7
    .quad   .L7
    .quad   .L7
    .quad   .L7
    .quad   .L7
    .quad   .L7
    .quad   .L7
    .quad   .L7
    .text
    .p2align 4,,10
    .p2align 3
.L7:
    call    _ZN5boost6detail7variant13forced_returnIiEET_v
    .p2align 4,,10
    .p2align 3
.L30:
    movl    $100, %eax
.L8:
    addq    $8, %rsp
    .cfi_remember_state
    .cfi_def_cfa_offset 8
    ret
    .p2align 4,,10
    .p2align 3
.L10:
    .cfi_restore_state
    movl    $999, %eax
    jmp .L8
    .cfi_endproc
.LFE9310:
    .size   _Z3runN5boost7variantI3FooJ3BarEEE, .-_Z3runN5boost7variantI3FooJ3BarEEE
    .section    .text.unlikely
.LCOLDE4:
    .text
.LHOTE4:
    .globl  _Z3runN5boost7variantI3FooI3BarEEE
    .set    _Z3runN5boost7variantI3FooI3BarEEE,_Z3runN5boost7variantI3FooJ3BarEEE
    .section    .text.unlikely
.LCOLDB5:
    .section    .text.startup,"ax",@progbits
.LHOTB5:
    .p2align 4,,15
    .globl  main
    .type   main, @function
main:
.LFB9320:
    .cfi_startproc
    movl    $100, %eax
    ret
    .cfi_endproc
.LFE9320:
    .size   main, .-main
    .section    .text.unlikely
.LCOLDE5:
    .section    .text.startup
.LHOTE5:
    .section    .rodata
    .align 32
    .type   _ZZN5boost6detail7variant13forced_returnIvEET_vE19__PRETTY_FUNCTION__, @object
    .size   _ZZN5boost6detail7variant13forced_returnIvEET_vE19__PRETTY_FUNCTION__, 58
_ZZN5boost6detail7variant13forced_returnIvEET_vE19__PRETTY_FUNCTION__:
    .string "T boost::detail::variant::forced_return() [with T = void]"
    .align 32
    .type   _ZZN5boost6detail7variant13forced_returnIiEET_vE19__PRETTY_FUNCTION__, @object
    .size   _ZZN5boost6detail7variant13forced_returnIiEET_vE19__PRETTY_FUNCTION__, 57
_ZZN5boost6detail7variant13forced_returnIiEET_vE19__PRETTY_FUNCTION__:
    .string "T boost::detail::variant::forced_return() [with T = int]"
    .ident  "GCC: (Ubuntu 5.3.0-3ubuntu1~14.04) 5.3.0 20151204"
    .section    .note.GNU-stack,"",@progbits

原文由 m.s. 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 636
1 个回答

.cfi 指令、未使用的标签和注释行是一个已解决的问题: Matt Godbolt 的编译器资源管理器 背后的脚本在 其 github 项目 上是开源的。它甚至可以进行颜色突出显示以将源代码行与 asm 行匹配(使用调试信息)。

您可以在本地设置它,这样您就可以使用所有 #include 路径等(使用 -I/... )向它提供属于您项目一部分的文件。因此,您可以在不想通过 Internet 发送的私有源代码上使用它。

Matt Godbolt 的 CppCon2017 演讲 “我的编译器最近为我做了什么? Unbolting the Compiler’s Lid” 展示了如何使用它(它非常不言自明,但如果您阅读 github 上的文档,它具有一些简洁的功能),以及 如何阅读 x86 asm ,并为所有初学者简要介绍了 x86 asm 本身,并查看编译器输出。他继续展示了一些简洁的编译器优化(例如除以常数),以及什么样的函数可以提供有用的 asm 输出以查看优化的编译器输出(函数 args,而不是 int a = 123; )。

在 Godbolt 编译器资源管理器中,如果您想取消选中指令的过滤器选项,例如因为您想查看 .section -g0 -fno-asynchronous-unwind-tables .p2align 编译器输出中的内容。默认情况下添加 -g 到您的选项中,以获取它用于颜色突出显示匹配源和 asm 行的调试信息,但这意味着 .cfi 每个堆栈操作的指令,并且 .loc 对于每个源代码行,等等。


对于普通的 gcc/clang(不是 g++), -fno-asynchronous-unwind-tables 避免了 .cfi 指令。可能也有用: -fno-exceptions -fno-rtti -masm=intel 。确保省略 -g

复制/粘贴此供本地使用

 g++ -fno-asynchronous-unwind-tables -fno-exceptions -fno-rtti -fverbose-asm \
    -Wall -Wextra  foo.cpp   -O3 -masm=intel -S -o- | less

或者 -Os 可以更具可读性,例如使用 div 除以非 2 的幂常数而不是乘法逆,即使这对性能来说要差很多而且只有一点点更小,如果有的话。


但实际上,我建议直接使用 Godbolt(在线或在本地设置)!您可以在 gcc 和 clang 版本之间快速切换,以查看新旧编译器是否做了一些愚蠢的事情。 (或者 ICC 做什么,甚至 MSVC 做什么。)甚至还有 ARM / ARM64 gcc 6.3,以及用于 PowerPC、MIPS、AVR、MSP430 的各种 gcc。 (看看在 int 比寄存器宽,或者不是 32 位的机器上发生的事情会很有趣。或者在 RISC 与 x86 上)。

对于 C 而不是 C++,您可以使用 -xc -std=gnu11 来避免将语言下拉菜单翻转为 C,这会重置您的源窗格和编译器选择,并提供一组不同的编译器。


用于制作供人类使用的 asm 的有用编译器选项

  • 请记住,您的代码只需要编译,而不是链接:将指针传递给外部函数,如 void ext(void*p) 是阻止优化的好方法。您只需要它的原型,没有定义,因此编译器无法内联它或对它的作用做出任何假设。 (或者 Benchmark::DoNotOptimize 可以强制编译器在寄存器中实现一个值,或者忘记它是一个已知常量,如果您足够了解 GNU C 内联汇编语法以使用约束来理解效果你有你对编译器的要求。)

  • 我建议使用 -O3 -Wall -Wextra -fverbose-asm -march=haswell 来查看代码。 ( -fverbose-asm 只会让源代码看起来很吵,但是,当你得到的只是编号的临时变量作为操作数的名称时。)当你摆弄源代码以查看它如何更改 asm 时,你 肯定 希望启用编译器警告。当解释是你做了一些值得在源代码中警告的事情时,你不想浪费时间在 asm 上摸索。

  • 要了解调用约定是如何工作的, 您通常希望查看没有内联的 caller 和 callee

您可以在定义上使用 __attribute__((noipa)) foo_t foo(bar_t x) { ... } 或使用 gcc -O3 -fno-inline-functions -fno-inline-functions-called-once -fno-inline-small-functions 编译以禁用内联。 (但这些命令行选项不会禁用克隆用于常量传播的函数 noipa = 没有过程间分析。它甚至比 __attribute__((noinline,noclone)) 更强。) 从编译器的角度来看,如何处理数组的引用,为什么不允许按值传递(而不是衰减)? 例如。

或者,如果您只想查看函数如何传递/接收不同类型的参数,您可以使用不同的名称但使用相同的原型,这样编译器就没有内联定义。这适用于任何编译器。如果没有定义,函数对优化器来说只是一个黑匣子,仅受调用约定/ABI 管理。

  • -ffast-math 将获得许多 libm 函数内联,一些到一条指令(尤其是 SSE4 可用于 roundsd )。有些将仅与 -fno-math-errno 或 --- 的其他“更安全”部分内 -ffast-math ,而没有允许编译器以不同方式舍入的部分。如果您有 FP 代码,请务必查看有无 -ffast-math 的情况。如果您无法在常规构建中安全地启用任何 -ffast-math ,也许您会想到可以在源代码中进行安全更改以允许在没有 -ffast-math 的情况下进行相同的优化 ---

  • -O3 -fno-tree-vectorize 将在没有自动矢量化的情况下进行优化,因此如果您想与 -O2 进行比较,您可以获得完全优化(它不会在 gcc11 和更早版本上启用自动矢量化,但在所有铛)。

-Os (针对大小和速度进行优化)有助于 保持代码更紧凑,这意味着要理解的代码更少。 clang’s -Oz optimizes for size even when it hurts speed, even using push 1 / pop rax instead of mov eax, 1 , so that’s only interesting for code高尔夫

甚至 -Og (最小优化)可能是您想要查看的内容,具体取决于您的目标。 -O0 充满了存储/重新加载噪音,这使得它更难遵循, 除非你使用 register vars 。唯一的好处是每个 C 语句都编译成一个单独的指令块,它使 -fverbose-asm 能够使用实际的 C var 名称。

  • 默认情况下,clang 展开循环,因此 -fno-unroll-loops 在复杂函数中很有用。您可以了解“编译器做了什么”,而无需涉足展开的循环。 (gcc 启用 -funroll-loops-fprofile-use ,但不能使用 -O3 )。 (这是对人类可读代码的建议,而不是对运行速度更快的代码的建议。)

  • 一定要启用某种程度的优化,除非您特别想知道 -O0 做了什么。它的“可预测的调试行为”要求使编译器在每个 C 语句之间存储/重新加载所有内容,因此您可以使用调试器修改 C 变量,甚至可以“跳转”到同一函数中的不同源代码行,并像您一样继续执行在 C 源代码中做到了这一点。 -O0 输出在存储/重新加载时非常嘈杂(而且速度如此之慢),这不仅是因为缺乏优化,还因为 强制取消优化以支持调试。 (也 相关)。


要混合使用 source 和 asm ,请使用 gcc -Wa,-adhln -c -g foo.c | less 将额外选项传递给 as 。 (在 博客文章另一个博客 中对此进行了更多讨论。)。请注意,此输出不是有效的汇编程序输入,因为 C 源代码直接在那里,而不是作为汇编程序注释。所以不要称它为 .s 。 A .lst 如果要将其保存到文件中可能有意义。

Godbolt 的颜色突出显示具有类似的目的,并且非常适合帮助您查看多个 不连续的 asm 指令何时来自同一源代码行。我根本没有使用过那个 gcc 列表命令,所以 IDK 它的性能如何,在这种情况下它是多么容易让眼睛看到。

我喜欢 Godbolt 的 asm 窗格的高代码密度,所以我认为我不希望将源代码行混入其中。至少对于简单的功能来说不是。也许有一个功能太复杂而无法处理 asm 的整体结构……


请记住,当您只想查看 asm 时, 请忽略 main() 和 compile-time constants 。您想查看在寄存器中处理函数 arg 的代码,而不是在常量传播将其变为 return 42 之后的代码,或者至少优化掉一些东西。

从函数中删除 static 和/或 inline 将为它们生成一个独立的定义,以及为任何调用者生成一个定义,因此您只需查看即可。

不要将代码放在名为 main() 的函数中。 gcc 知道 main 是特殊的并且假设它只会被调用一次,所以它将它标记为“冷”并对其进行较少的优化。


您可以做的另一件事:如果您确实制作了 main() ,您可以运行它并使用调试器。 stepi ( si ) 按指令逐步执行。有关说明,请参阅 x86 标签 wiki 的底部。但请记住,在使用编译时常量参数内联到 main 之后,代码可能会优化掉。

__attribute__((noinline)) 可能对您不想内联的函数有所帮助。 gcc 还将为知道它们正在传递一个常量的调用站点制作函数的常量传播克隆,即具有一个 args 作为常量的特殊版本。符号名称将是 .clone.foo.constprop_1234 或 asm 输出中的名称。您也可以使用 __attribute__((noclone)) 禁用它。)。


例如

如果您想查看编译器如何将两个整数相乘:我将以下代码 放在 Godbolt 编译器资源管理器上 以获取 asm(来自 gcc -O3 -march=haswell -fverbose-asm ),以错误的方式和正确的方式进行测试。

 // the wrong way, which people often write when they're used to creating a runnable test-case with a main() and a printf
// or worse, people will actually look at the asm for such a main()
int constants() { int a = 10, b = 20; return a * b; }
    mov     eax, 200  #,
    ret                     # compiles the same as  return 200;  not interesting

// the right way: compiler doesn't know anything about the inputs
// so we get asm like what would happen when this inlines into a bigger function.
int variables(int a, int b) { return a * b; }
    mov     eax, edi  # D.2345, a
    imul    eax, esi        # D.2345, b
    ret

(这种 asm 和 C 的混合是通过将 godbolt 的 asm 输出复制粘贴到正确的位置来手工制作的。我发现这是展示短函数如何在 SO 答案/编译器错误报告/电子邮件中编译的好方法。)

原文由 Peter Cordes 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题