【问题标题】:How to remove "noise" from GCC/clang assembly output?如何从 GCC/clang 程序集输出中删除“噪音”?
【发布时间】:2016-11-27 20:56:39
【问题描述】:

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

【问题讨论】:

  • gcc 不会因为没有更好的事情可做而生成无用的代码。为了正确构建和链接 C++ 源代码,需要所有这些“噪音”:boost 的所有包袱的 RTTI 等等……如果你想摆脱所有这些噪音,请不要使用 boost。
  • 相信你可以看看Godbolt是如何调用gcc并清除剩余的噪音
  • 所以,编写一个简单的 Perl 脚本来去除不需要的绒毛。
  • @Sam:很多标签,比如.LCOLDE3: / .LHOTE3: 都是纯噪声。我认为它们不会影响目标文件,甚至不会影响符号表或其他元数据。 (是的,剥离它是一个已解决的问题:godbolt.org 背后的脚本在 github 上是开源的)。对于查看代码,我还会推荐gcc.godbolt.org(与-O3 -Wall -Wextra -march=...)。但请记住,当您只想查看 asm 时,请忽略 main() 并使用编译时常量调用它,这样您就可以查看处理函数 arg 的代码。
  • g++ -g -O3 -std=c++14 -c test.cc -o test.o &amp;&amp; objdump -dS test.o

标签: c++ gcc assembly clang


【解决方案1】:

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

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

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


对于普通的 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

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

对于 C 而不是 C++,使用 -xc -std=gnu11 或其他东西;编译器资源管理器站点仅提供 g++/clang++,不提供 gcc/clang。 (或者您可以在语言下拉菜单中使用 C 模式,但它有不同的编译器选择,这些编译器大多受到限制。它会重置您的源代码窗格,因此在 C 和 C++ 之间切换更像是一种考验。)


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

  • 请记住,您的代码只需要编译,而不需要链接:将指针传递给像 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)) 更强大。)请参阅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 的其他“更安全”部分内联,而没有允许编译器以不同方式舍入的部分。如果你有 FP 代码,一定要看看有/没有-ffast-math。如果您无法在常规构建中安全地启用任何-ffast-math,也许您会想到可以在源代码中进行安全更改以允许在没有-ffast-math 的情况下进行相同的优化。

  • -O3 -fno-tree-vectorize 将在不进行自动矢量化的情况下进行优化,因此如果您想与 -O2 进行比较,则无需进行完全优化(在 gcc 上不启用自动矢量化,但在 clang 上启用)。

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

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


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

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

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


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

从函数中删除 static 和/或 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 答案/编译器错误报告/电子邮件中编译的好方法.)

【讨论】:

  • 您可以通过-fno-asynchronous-unwind-tables禁用CFI指令。
  • __attribute__((noipa)) 也可能值得一提;它包含noinlinenoclone,基本上让你回到调用者将函数视为黑匣子的地方。它用于编译器调试,因此也非常适合实验。
【解决方案2】:

您始终可以从目标文件中查看生成的程序集,而不是使用编译器的程序集输出。 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 语法(可选

【讨论】:

  • 我喜欢 objdump -Mintel -drw 在反汇编 .o 文件时显示重定位的符号名称 (-r),而不是对多字节指令的机器代码进行换行。这有时更具可读性,但可读性可能会低得多,因为您会丢失分支目标上的标签,以及其他一些信息。 (Agner Fog 的 objconv 反汇编器将为反汇编中的分支目标制作标签)。这也意味着您无法从gcc -fverbose-asm 中受益。
  • 在 GCC 生成程序集列表时,是否没有将程序集与源代码混合的选项? MSVC 有这个选项(和其他几个,非常方便)。并且生成的代码实际上非常干净易读,一旦您滚动浏览顶部的模板和标准库 goo。
  • @CodyGray:事实证明,使用 GNU as 的选项是可能的。请参阅我的答案的更新。不过,当我想知道这一点时,我通常只会查看启用了颜色突出显示的 Godbolt。与源代码行关联的 asm 指令并不总是连续的...
  • Script for converting GNU -Mintel syntax to more exact NASM syntax,即将byte ptr 替换为byte,也许还有其他一些小问题。
  • @PeterCordes 认为脚本是有害的,因为它不仅不完整,而且还会从反汇编中删除重要信息,请参阅我对该帖子的评论。
【解决方案3】:

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

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

我还没有遇到过这个问题,但是asm volatile 对编译器的优化器来说可能非常困难,因为它往往不会影响此类代码。

【讨论】:

  • 你确定你需要volatile吗?您是否注意到编译器会在没有它的情况下优化掉这些标签?
  • @CodyGray 我没试过。我假设您需要volatile,因为优化器会在 DCE 期间将其丢弃。
  • 您实际上并不需要 volatile,因为 GCC 会假定它,因为 asm 语句没有输出操作数。
  • "asm 没有输出操作数的语句,包括asm goto 语句,是隐式易失的。" gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html#Volatile
  • 您可以使用 asm cmets (asm("#start of something")),但在这种情况下发出甚至不组装的 asm 可能是一个好主意。它确保您不会不小心将其留在实际构建中并 gimp 优化器。此外,我喜欢您使用扩展 asm 语法在语句的每个克隆上放置一个唯一 ID 的想法,这有助于查找对函数的多个调用。不过,我不确定volatile 是否会阻止优化器进行克隆。也许它可以被克隆,只要它仍然运行正确的次数。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-09-08
  • 2014-10-24
  • 2012-08-22
  • 1970-01-01
  • 2012-01-14
  • 1970-01-01
相关资源
最近更新 更多