如何从GCC/clang汇编输出中去除"噪音"?

114
我希望检查在我的代码中应用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

11
GCC不会生成无用的代码,只是因为它没有更好的事情可做。所有这些“噪音”都是为了正确构建和链接C++源代码而需要的:boost的所有负担、RTTI等等……如果你想要摆脱所有这些噪音,就不要使用boost。 - Sam Varshavchik
4
我相信你可以看一下Godbolt是如何调用gcc并清除剩余的噪音的。 - phuclv
3
请写一个简单的 Perl 脚本,以去除不需要的内容。 - Sam Varshavchik
13
@Sam: 很多标签,比如.LCOLDE3:.LHOTE3:几乎是纯粹的噪音。我认为它们不会影响目标文件,甚至不会影响符号表或其他元数据。(而且是的,剥离它们已经是一个解决的问题:godbolt.org背后的脚本在GitHub上是开源的)。我也推荐使用http://gcc.godbolt.org/(使用`-O3 -Wall -Wextra -march=...选项)来查看代码。但要记住,如果你只想查看汇编代码,请省略main()`并使用编译时常量调用它,这样你就可以只查看处理函数参数的代码。 - Peter Cordes
4
使用以下命令对test.cc进行编译,并生成一个名为test.o的目标文件,然后使用objdump工具以汇编和源代码的形式显示该目标文件的内容:g++ -g -O3 -std=c++14 -c test.cc -o test.o && objdump -dS test.o - Leandros
显示剩余3条评论
3个回答

146
剥离掉.cfi指令、未使用的标签和注释行是一个已解决的问题:Matt Godbolt's compiler explorer背后的脚本在其github项目上是开源的。它甚至可以进行颜色高亮,将源代码行与汇编代码行匹配(使用调试信息)。

您可以在本地设置它,以便您可以通过所有的#include路径等来提供项目中的文件给它使用(使用-I/...)。这样,您可以在私有源代码上使用它,而不必将其发送到互联网上。

Matt Godbolt的CppCon2017演讲“最近编译器为我做了什么?解开编译器的盖子”展示了如何使用它(如果您阅读github文档,则相当容易理解,还具有一些很好的功能),以及如何阅读x86汇编代码,对于完全的新手来说,它也对x86汇编本身进行了简单介绍,并且可以查看编译器输出。接下来,他展示了一些很棒的编译器优化方法(例如针对常数进行除法运算),以及哪些类型的函数能够产生有用的汇编代码输出(函数参数,而不是int a = 123;)。

在Godbolt编译器浏览器上,如果你想取消指令过滤选项,例如因为你想在编译器输出中看到`.section`和`.p2align`的内容,那么使用`-g0 -fno-asynchronous-unwind-tables`可能会很有用。默认情况下,你需要在选项中添加`-g`来获取调试信息,以便对匹配的源代码和汇编代码进行颜色高亮显示,但这意味着每个堆栈操作都会有`.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可能更易读,例如对于非2的幂常数的除法使用div而不是乘法逆元,尽管这在性能上要差得多,并且只有稍微小一点(如果有的话)。

在 Godbolt 上使用-g0,如果你想取消选中"filter directives",以查看被错误过滤的部分、全局变量等。它的默认值-g会使事情变得混乱,所以-g0可以抵消它。(-g0是没有命令行选项时的默认值)。还可以使用-fno-asynchronous-unwind-tables,也许还有-fno-exceptions -fno-rtti


但是实际上,我建议直接使用Godbolt(在线或本地设置)!您可以快速切换gcc和clang的版本,以查看旧版或新版编译器是否会出现一些愚蠢的问题。(或者ICC会做什么,甚至MSVC会做什么)。甚至还有ARM / ARM64 gcc 6.3,以及各种用于PowerPC、MIPS、AVR、MSP430的gcc。(在一个整数比寄存器宽或不是32位的机器上看到发生了什么,或者在RISC与x86之间的机器上看到发生了什么,这可能很有趣)。
对于C而不是C++,您可以使用“-xc -std=gnu11”来避免切换语言下拉菜单到C,这会重置您的源代码窗格和编译器选择,并且有不同的可用编译器。

用于生成易读汇编代码的有用编译器选项

记住,你的代码只需要编译通过,不需要链接:将指向外部函数的指针传递给void ext(void*p)这样的函数是阻止优化的好方法。你只需要一个原型,没有定义,这样编译器就无法内联它或对其进行任何假设。(或者使用类似Benchmark::DoNotOptimize的内联汇编可以强制编译器在寄存器中实例化一个值,或者忘记它是一个已知常量,如果你足够了解GNU C内联汇编语法,可以使用约束来理解你对编译器的要求产生的影响。)
我建议使用-O3 -Wall -Wextra -fverbose-asm -march=haswell来查看代码。(当所有你得到的都是以数字为名称的临时变量作为操作数时,-fverbose-asm可能会使源代码看起来很嘈杂。)当你调整源代码以查看它如何改变汇编代码时,你绝对希望启用编译器警告。当你在源代码中做出应该引发警告的操作时,你不想浪费时间纠结于汇编代码的解释。
为了了解调用约定的工作原理,你经常希望在不进行内联的情况下查看调用者和被调用者。你可以在定义上使用__attribute__((noipa)) foo_t foo(bar_t x) { ... },或者使用gcc -O3 -fno-inline-functions -fno-inline-functions-called-once -fno-inline-small-functions进行编译以禁用内联。(但是这些命令行选项不会禁用为常量传播克隆函数。noipa = no Inter-Procedural Analysis. 它甚至比__attribute__((noinline,noclone))更强大。)参见From compiler perspective, how is reference for array dealt with, and, why passing by value(not decay) is not allowed?中的示例。
或者,如果你只想查看函数如何传递/接收不同类型的参数,你可以使用不同的名称但相同的原型,这样编译器就没有定义来进行内联。这适用于任何编译器。没有定义,函数对优化器来说只是一个黑盒子,仅受调用约定/ABI的控制。 -ffast-math将使许多libm函数内联,其中一些甚至只有一条指令(特别是当SSE4可用于roundsd时)。有些函数只需使用-fno-math-errno或其他“更安全”的-ffast-math部分进行内联,而不包括允许编译器以不同方式舍入的部分。如果你有浮点代码,一定要使用/不使用-ffast-math来查看它。如果在常规构建中无法安全启用任何-ffast-math,也许你会得到一个在源代码中进行相同优化而不需要-ffast-math的安全更改的想法。 -O3 -fno-tree-vectorize将在不自动向量化的情况下进行优化,因此如果你想与-O2进行比较,可以获得完全的优化(在gcc11及之前的版本上不会启用自动向量化,但在所有clang版本上都会启用)。 -Os(优化大小和速度)可能会有所帮
要获取源代码和汇编代码的混合,可以使用gcc -Wa,-adhln -c -g foo.c | less来传递额外的选项给as。(更多讨论请参见一篇博客文章另一篇博客。)请注意,这个输出不是有效的汇编器输入,因为C源代码直接存在,而不是作为汇编器注释。所以不要称它为.s。如果你想将其保存到文件中,.lst可能是有意义的。
Godbolt的颜色高亮功能具有类似的目的,并且非常适合帮助您看到多个非连续的汇编指令来自同一行源代码。我根本没有使用过那个gcc列表命令,所以不知道它的效果如何,以及在这种情况下是否容易被眼睛看到。
我喜欢godbolt的汇编窗格的高代码密度,所以我不认为我会喜欢混合源代码行。至少对于简单的函数来说是这样。也许对于一个过于复杂以至于无法掌握汇编整体结构的函数来说,可能会有所帮助...
记住,当你只想查看汇编代码时,忽略掉main()和编译时常量。你想要看的是处理寄存器中函数参数的代码,而不是在常量传播将其转换为return 42之后的代码,或者至少优化掉一些东西。
从函数中移除static和/或inline将为它们生成一个独立的定义,以及任何调用者的定义,所以你可以直接查看那部分代码。
不要把你的代码放在一个名为main()的函数中。gcc知道main是特殊的,并假设它只会被调用一次,因此将其标记为“冷”并进行较少的优化。
另外一件你可以做的事情是:如果你确实写了一个main()函数,你可以运行它并使用调试器。stepisi)按照指令进行步进。请参考 tag wiki底部的指令说明。但要记住,代码可能会在编译时将常量参数内联到主函数中后进行优化。 __attribute__((noinline))可能有所帮助,用于不希望被内联的函数。gcc还会为函数创建常量传播的克隆版本,即对于知道自己传递常量的调用点,会创建一个特殊版本。汇编输出中的符号名称将为.clone.foo.constprop_1234或类似形式。你也可以使用__attribute__((noclone))来禁用这个功能。
例如
如果你想看看编译器如何将两个整数相乘:我在以下代码中在Godbolt编译器浏览器上放置了这段代码,以获取错误和正确的测试方法的汇编代码(来自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

这个混合使用汇编语言和C语言的代码是通过将godbolt的汇编输出手动复制粘贴到正确的位置而生成的。我发现这是一种很好的方式,可以展示一个短函数在SO答案、编译器错误报告和电子邮件中的编译情况。

5
您可以通过 -fno-asynchronous-unwind-tables 禁用CFI指令。 - edmz
2
attribute((noipa)) 也值得一提;它包含了 noinlinenoclone,基本上让调用者将函数视为黑盒子。它旨在用于编译器调试,因此非常适合进行实验。 - Nate Eldredge

17

您可以查看对象文件生成的汇编代码,而不是使用编译器的汇编输出。 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:选择 Intel 语法而不是丑陋的 AT&T 语法(可选

5
我喜欢使用 objdump -Mintel -drw 来反汇编 .o 文件,它可以展示重定位符号名称 (-r) 且不会对多字节指令进行换行,因此有时更易读。但由于失去了分支目标的标签以及其他一些信息,有时也会导致不太易读。(Agner Fog 的 objconv 反汇编器可以为分支目标创建标签)。这也意味着你无法从 gcc -fverbose-asm 中受益。 - Peter Cordes
1
GCC在生成汇编列表时是否没有与源代码混合的选项?MSVC有这个选项(还有其他几个非常方便的选项)。一旦您向下滚动模板和标准库开头处的良莠混杂,生成的代码实际上非常清晰易读。 - Cody Gray
1
@CodyGray:事实证明,使用GNU as选项是可能的。请参见我的答案更新。但是,当我想知道这一点时,我通常只需查看启用了颜色突出显示的godbolt。与源行相关联的汇编指令并不总是连续的... - Peter Cordes
1
将GNU的-Mintel语法转换为更精确的NASM语法的脚本,即用byte替换byte ptr,可能还有其他一些小细节。参考链接 - Peter Cordes
1
@PeterCordes 那个脚本是有害的,因为它不仅不完整,而且会从反汇编中剥离重要信息,请参见我在该帖子中的评论。 - Ruslan
@Ruslan:谢谢你提醒我,我从未在实践中尝试过。 - Peter Cordes

12

我喜欢插入标签,以便我可以轻松地从objdump输出中进行grep。

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

我目前还没有遇到过这个问题,但是asm volatile可能会对编译器的优化器造成很大压力,因为它倾向于不对这样的代码进行修改。


@CodyGray 我还没有尝试过。我会认为你需要 volatile,因为优化器会在 DCE 期间将其丢弃。 - Tim
2
你实际上不需要使用 volatile,因为GCC会假定它,因为asm语句没有输出操作数。 - Ross Ridge
@RossRidge 有意思。这是有文档记录的行为吗? - Tim
5
没有输出操作数的 asm 语句,包括 asm goto 语句,在编译器中被隐式地视为 volatile - Ross Ridge
3
您可以使用汇编注释(asm("#something的开始")),但在这种情况下,发出甚至不能组装的汇编代码可能是个好主意。这确保了您不会意外地将其留在实际构建中并阻碍优化器。同时,我喜欢您使用扩展汇编语法为每个语句克隆放置一个唯一的标识符的想法,以帮助查找对函数的多次调用。但我不确定volatile是否能阻止优化器进行克隆。也许它可以被克隆,只要它仍然运行正确的次数就可以。 - Peter Cordes
显示剩余2条评论

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