1. ホーム
  2. c++

[解決済み] GCC/clangのアセンブリ出力から "ノイズ "を取り除くには?

2023-04-14 02:06:26

質問

を適用した場合のアセンブリ出力を検査したいのですが。 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にそのすべての"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    $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

どのように解決するのですか?

を削除して .cfi ディレクティブ、使われていないラベル、コメント行を取り除くことは、解決された問題です。 Matt Godbolt のコンパイラエクスプローラ はオープンソースで github プロジェクト . デバッグ情報を使って、ソース行とasm行を一致させるカラーハイライトも可能です。

ローカルに設定することで、プロジェクトの一部であるファイルに、すべての #include のパスなどを含むプロジェクトのファイルを与えることができます (これは -I/... ). そのため、インターネット上で送信したくないプライベートなソースコードに使用することができます。

マット・ゴッドボルトのCppCon2017の講演内容 "最近、私のコンパイラは私のために何をしてくれたのか?コンパイラの蓋を開ける" は、その使い方を示しています。 (かなり自明ですが、githubのドキュメントを読めばいくつかの巧妙な機能があります)、そしてまた x86 asmの読み方 で、全くの初心者のためにx86 asmそのものと、コンパイラの出力の見方について優しく紹介しています。 さらに、コンパイラの最適化(例えば定数で割る)や、最適化されたコンパイラの出力を見るために、どのような関数が有用なasmの出力を与えるか(関数の引数、ではなく 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 のバージョンをすばやく切り替えて、古いコンパイラーや新しいコンパイラーが何かおかしなことをしていないかどうかを確認することができます。 (ARM / ARM64 gcc 6.3や、PowerPC, MIPS, AVR, MSP430用の様々なgccがあります。 (以下のようなマシンで何が起こるかを見るのは面白いかもしれません。 int がレジスタより広い、あるいは 32 ビットでないマシンで何が起こるかを見るのは興味深いことです。 または、RISC と x86 の比較)。

C++ の代わりに C を使用する場合は -xc -std=gnu11 コンパイラエクスプローラのサイトでは gcc / clang ではなく g++ / clang++ しか提供されていません。 (または、言語ドロップダウンで C モードを使用することもできますが、その場合はコンパイラの選択が異なり、ほとんどがより限定されたものになります。 また、ソース ペインがリセットされるため、C と C++ の間を切り替えるのはより困難です)。


人間が食べるためのasmを作るのに便利なコンパイラのオプション :

  • あなたのコードはコンパイルするだけで、リンクする必要はないことを忘れないでください。 void ext(void*p) のような外部関数へのポインタを渡すことは、最適化から何かを止めるための良い方法です。 . あなたは、コンパイラがそれをインライン化したり、それが何をするかについて仮定することができないように、定義のない、それのためのプロトタイプを必要とするだけです。 (あるいは のような asm をインライン化することもできます。 Benchmark::DoNotOptimize はコンパイラにレジスタの値を実体化させたり、既知の定数であることを忘れさせることができます。もしあなたがGNU C inline 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 = Inter-Procedural Analysisを無効にします。 よりもさらに強く __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 (これはgccでは自動ベクトル化を有効にしませんが、clangでは有効にします)。

  • clangはデフォルトでループをアンロールするので -fno-unroll-loops は複雑な関数で有用です . ループを展開することなく、コンパイラが何をしたかを知ることができます。 (gccでは -funroll-loops-fprofile-use とは異なり -O3 ). (これは人間が読みやすいコードのための提案であり、より速く実行されるコードのためのものではありません)。

  • を特に知りたいのでなければ、間違いなく、ある程度の最適化を可能にします。 -O0 が行いました。 . その "predictable debug behaviour" の要件により、コンパイラーはすべての C ステートメント間ですべてを保存/再ロードするため、デバッガーで C 変数を変更したり、同じ関数内で別のソース行に "jump" して、C ソース内でそれを行ったかのように実行を継続することができます。 -O0 の出力がストアやリロードでうるさい (そして遅い) のは、最適化がされていないだけでなく デバッグをサポートするために強制的に最適化を解除しています。 . (また 関連 ).


ソースとasmの混在を取得するために を使用します。 gcc -Wa,-adhln -c -g foo.c | less に追加オプションを渡すために as . (これについての詳しい説明は ブログ記事 で、そして 別のブログ .). C のソースはアセンブラのコメントとしてではなく、直接そこにあるので、この出力は有効なアセンブラの入力ではないことに注意してください。 ですから、これを .s . A .lst は、それをファイルに保存したい場合に意味をなすかもしれません。

Godbolt のカラーハイライトも同じような目的で使用され、複数の 非連続 asm 命令が同じソース ラインから来たときに、それを見分けるのに役立ちます。 私はその gcc リスト コマンドをまったく使用したことがないので、その場合、それがどの程度うまく機能し、目にとってどの程度簡単であるかは IDK です。

私は godbolt の asm ペインの高いコード密度が好きなので、ソース行が混在しているのは好ましくないと思っています。 少なくとも、単純な関数についてはそうではありません。 多分、asm が行うことの全体的な構造を把握するには複雑すぎる機能では...。


そして、asmを見るだけでいいという時に思い出してください。 を省き main() とコンパイル時の定数 . あなたが見たいのは、レジスタ内の関数argを扱うコードであって、定数伝搬で return 42 に変えた後のコードではなく、少なくともいくつかのものを最適化した後のコードを見たいのです。

削除する static または inline は、関数から独立した定義を生成し、呼び出し元の定義も生成するので、それを見るだけです。

という関数にコードを書かないようにしましょう。 main() gcc は main は特別なもので、一度だけ呼ばれると仮定しているので、 "cold"としてマークし、あまり最適化しないようにします。


もうひとつ、できることがあります。もし、あなたが main() を作った場合、それを実行してデバッガを使うことができます。 stepi ( si ) のステップをインストラクションごとに説明します。 の下を参照してください。 x86 タグの wiki を参照してください。 しかし、コンパイル時定数の引数でmainにインライン化した後、コードが最適化されなくなる可能性があることを忘れないでください。

__attribute__((noinline)) は、インライン化されないようにしたい関数で、役に立つかもしれません。 gccはまた、定数を渡していることを知っている呼び出し元に対して、定数伝播型の関数のクローン、つまり、引数の1つを定数とした特別なバージョンを作成します。 シンボル名は .clone.foo.constprop_1234 などと出力されます。あなたは __attribute__((noclone)) を使ってそれを無効にすることもできます)。


例えば

コンパイラがどのように2つの整数を乗算しているかを見たい場合。 次のようなコードを書いてみました。 を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の回答やコンパイラのバグレポート、メールなどで短い関数がどのようにコンパイルされるかを示すには良い方法だと思います)。