如何从 GCC/clang 汇编输出中删除 "noise"?

How to remove "noise" from GCC/clang assembly output?

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

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

(...)
main:
.LFB9320:
    .cfi_startproc
    movl    0, %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 删除所有 "noise" 并只输出程序 运行 时实际调用的内容?


完整的汇编输出:

    .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    , %rsp
    .cfi_def_cfa_offset 16
    movl    $_ZZN5boost6detail7variant13forced_returnIvEET_vE19__PRETTY_FUNCTION__, %ecx
    movl    , %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    , %rsp
    .cfi_def_cfa_offset 16
    movl    $_ZZN5boost6detail7variant13forced_returnIiEET_vE19__PRETTY_FUNCTION__, %ecx
    movl    , %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    , %rsp
    .cfi_def_cfa_offset 16
    movl    (%rdi), %eax
    cltd
    xorl    %edx, %eax
    cmpl    , %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    0, %eax
.L8:
    addq    , %rsp
    .cfi_remember_state
    .cfi_def_cfa_offset 8
    ret
    .p2align 4,,10
    .p2align 3
.L10:
    .cfi_restore_state
    movl    9, %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    0, %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

删除 .cfi 指令、未使用的标签和注释行是一个已解决的问题:Matt Godbolt's compiler explorer are open source on its github project 背后的脚本。它甚至可以进行颜色突出显示以将源代码行与 asm 行匹配(使用调试信息)。

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

Matt Godbolt 的 CppCon2017 演讲 “What Has My Compiler Done for Me Lately? Unbolting the Compiler's Lid” 展示了如何使用它(它不言自明,但如果您阅读 [=214= 上的文档,它有一些简洁的功能]),以及 如何阅读 x86 asm,为初学者简要介绍 x86 asm 本身,并查看编译器输出。他继续展示了一些整洁的编译器优化(例如除以常量),以及什么样的函数提供有用的 asm 输出以查看优化的编译器输出(函数参数,而不是 int a = 123;)。

在 Godbolt 编译器资源管理器上,如果您想取消选中指令的过滤器选项,使用 -g0 -fno-asynchronous-unwind-tables 会很有用,例如因为你想在编译器输出中看到 .section.p2align 的东西。默认设置是将 -g 添加到您的选项中,以获取它用于以颜色突出显示匹配的源代码和 asm 行的调试信息,但这意味着每个堆栈操作的 .cfi 指令,以及 .loc对于每个源代码行,等等。


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

Copy/paste 本地使用:

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 的有用编译器选项:

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

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

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

    您可以在定义上使用 __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代码,一定要看看with/without-ffast-math。如果您无法在常规构建中安全地启用任何 -ffast-math,也许您会想到一个安全更改的想法,您可以在源代码中进行安全更改,以允许在没有 -ffast-math.[= 的情况下进行相同的优化。 90=]

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

    -Os(优化大小和速度)有助于保持代码更紧凑,这意味着要理解的代码更少。 clang 的 -Oz 优化大小,即使它会影响速度,甚至使用 push 1 / pop rax 而不是 mov eax, 1,所以这只对 code golf.[=90 有意义=]

    甚至 -Og(最小优化)也可能是您想要查看的内容,具体取决于您的目标。 -O0 充满了 store/reload 噪音,这让人难以理解,unless you use register vars。唯一的好处是每个 C 语句都编译成一个单独的指令块,这使得 -fverbose-asm 能够使用实际的 C var 名称。

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

  • 绝对启用某种程度的优化,除非您特别想知道 -O0 做了什么。它的“可预测调试行为”要求使编译器 store/reload 每个 C 语句之间的所有内容,因此您可以使用调试器修改 C 变量,甚至“跳转”到同一函数内的不同源代码行,并继续执行如果你在 C 源代码中这样做。 -O0 输出在 stores/reloads 时非常嘈杂(而且很慢),这不仅仅是因为缺乏优化,而是 forced de-optimization to support debugging. (also related).


要混合使用源代码和 asm,请使用 gcc -Wa,-adhln -c -g foo.c | less to pass extra options to as. (More discussion of this in a blog post, and another blog。)请注意,此输出不是有效的汇编程序输入,因为 C 源代码直接存在,而不是作为汇编程序注释。所以不要称它为 .s。如果要将其保存到文件,.lst 可能有意义。

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

我喜欢 godbolt 的 asm 面板的高代码密度,所以我不希望混合源代码行。至少对于简单的函数不会。也许使用太复杂的函数无法处理 asm 所做的整体结构...


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

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

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


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

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


例如

如果你想看看编译器是如何将两个整数相乘的:我把下面的代码on the Godbolt compiler explorer来获取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 答案/编译器错误报告中展示短函数如何编译的好方法/ 电子邮件。)

您始终可以从目标文件中查看生成的程序集,而不是使用编译器程序集输出。 objdump 想到了。

您甚至可以告诉 objdump 将源代码与程序集混合,从而更容易找出源代码行对应于哪些指令。示例会话:

$ cat test.cc
int foo(int arg)
{
    return arg * 42;
}

$ g++ -g -O3 -std=c++14 -c test.cc -o test.o && objdump -dS -M intel test.o

test.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <_Z3fooi>:
int foo(int arg)
{
    return arg + 1;
   0:   8d 47 01                lea    eax,[rdi+0x1]
}
   3:   c3                      ret    

objdump 标志的解释:

  • -d反汇编所有可执行部分
  • -S 将程序集与源代码混合(使用 g++ 编译时需要 -g
  • -M intel 选择英特尔语法而不是丑陋的 AT&T 语法(可选

我喜欢插入可以轻松从 objdump 输出中 grep 出来的标签。

int main() {
    asm volatile ("interesting_part_begin%=:":);
    do_something();
    asm volatile ("interesting_part_end%=:":);
}

我还没有遇到过这方面的问题,但是 asm volatile 对编译器的优化器来说可能非常困难,因为它往往会保持这些代码不变。