# LTOでのインライン展開 [base::debug::Alias](/Nr6YGOpET_qTEBYPAkTUtw)はリンク時の最適化で変数が消滅することを避けるためのクラスだった。 変数が使われていないように見えても、Stack Traceで見たかったりログ・デバッグ目的で残っててほしいことは多い。そんなときに`Alias(var)`とすると`var`が消えないようになるというクラスだった。 ところで、LTOでどういうときに関数が消えるのか実際にみてみようと思う。 ## LTOを使うには 今回使用するのはGNU(g++)コンパイラ。 普通にコンパイルするとLTOはenableされていない。 そこで[`-flto`](https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html)というオプションを追加すると最適化が走るようになる。 ``` gcc -c -O2 -flto foo.c gcc -c -O2 -flto bar.c gcc -o myprog -flto -O2 foo.o bar.o ``` `-c`はオブジェクトファイルを生成するためのコマンド。これがないとシンボル解決までしようとして失敗する。 上記のように各オブジェクトファイルをそれぞれ作っていくが、この際同じオプションを使うことが推奨されている。 [Optimization Options](https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html)の中には他にもLTO関連のフラグがあるが、今回は`-flto`に絞って確認する。 ## やってみた 今回使用するサンプルコードは以下。 ```cpp // main.cc void Alias(const void* var); int main(void) { int a = 1; Alias(&a); return 0; } // alias.cc void Alias(const void* var) {} ``` 観てわかるとおり、Aliasの中身はないので、最適化でAliasの呼び出しそのものが消えるのが予想。 ### LTOなし まずはLTOなしバージョンを確認する。 以下のコマンドでコンパイルしてみる。 ``` g++ -c -O2 main.cc g++ -c -O2 alias.cc g++ -O2 main.o alias.o ``` これでできた`a.out`に対して解析をかける。 オブジェクトファイルや実行ファイルの中身は`objdump -d`で確認することができる。 `objdump -d a.out`の結果を見てみると実際にAliasがいた。 以下mainとAliasの中だけ抜粋。 ``` 0000000000001040 <main>: 1040: 48 83 ec 18 sub $0x18,%rsp 1044: 48 8d 7c 24 0c lea 0xc(%rsp),%rdi 1049: c7 44 24 0c 01 00 00 movl $0x1,0xc(%rsp) 1050: 00 1051: e8 fa 00 00 00 call 1150 <_Z5AliasPKv> 1056: 31 c0 xor %eax,%eax 1058: 48 83 c4 18 add $0x18,%rsp 105c: c3 ret 105d: 0f 1f 00 nopl (%rax) ... 0000000000001150 <_Z5AliasPKv>: 1150: c3 ret ``` `_Z5AliasPKv`がAlias関数のシンボル名で、mainの`call`命令で呼び出されている。 ちなみに各サイズは ``` -rw-r--r-- 1 elkurin primarygroup 31 Oct 12 21:38 alias.cc -rw-r--r-- 1 elkurin primarygroup 1080 Oct 12 21:40 alias.o -rwxr-xr-x 1 elkurin primarygroup 15896 Oct 12 21:40 a.out -rw-r--r-- 1 elkurin primarygroup 87 Oct 12 21:39 main.cc -rw-r--r-- 1 elkurin primarygroup 1312 Oct 12 21:39 main.o ``` ### -fltoつき 以下のコマンドでコンパイルしてみる。 ``` g++ -c -O2 -flto main.cc g++ -c -O2 -flto alias.cc g++ -O2 -flto main.o alias.o ``` 先程と同様に`objdump -d a.out`の結果を見てみると全然変わった。 ``` 0000000000001040 <main>: 1040: 31 c0 xor %eax,%eax 1042: c3 ret 1043: 66 2e 0f 1f 84 00 00 cs nopw 0x0(%rax,%rax,1) 104a: 00 00 00 104d: 0f 1f 00 nopl (%rax) ``` mainの中身は小さく、またAliasシンボルは消滅した。 ちなみに各サイズは ``` -rw-r--r-- 1 elkurin primarygroup 31 Oct 12 21:38 alias.cc -rw-r--r-- 1 elkurin primarygroup 3536 Oct 12 21:47 alias.o -rwxr-xr-x 1 elkurin primarygroup 15800 Oct 12 21:47 a.out -rw-r--r-- 1 elkurin primarygroup 87 Oct 12 21:39 main.cc -rw-r--r-- 1 elkurin primarygroup 3784 Oct 12 21:47 main.o ``` ## オブジェクトファイルのサイズについて 前のセクションの結果から、LTOなしのセクションと比べるとLTOありではオブジェクトファイルがめっちゃデカくなっていることがわかる。 なんでデカくなっているのか調べるためにオブジェクトファイルのセクションを見てみる。 セクションは`objdump -x`で列挙してくれる。 まずはLTOなし。 ``` $ objdump -x main.o ... Sections: Idx Name Size VMA LMA File off Algn 0 .text 00000000 0000000000000000 0000000000000000 00000040 2**0 CONTENTS, ALLOC, LOAD, READONLY, CODE 1 .data 00000000 0000000000000000 0000000000000000 00000040 2**0 CONTENTS, ALLOC, LOAD, DATA 2 .bss 00000000 0000000000000000 0000000000000000 00000040 2**0 ALLOC 3 .text.startup 0000001d 0000000000000000 0000000000000000 00000040 2**4 CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE 4 .comment 0000001f 0000000000000000 0000000000000000 0000005d 2**0 CONTENTS, READONLY 5 .note.GNU-stack 00000000 0000000000000000 0000000000000000 0000007c 2**0 CONTENTS, READONLY 6 .eh_frame 00000030 0000000000000000 0000000000000000 00000080 2**3 CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA SYMBOL TABLE: 0000000000000000 l df *ABS* 0000000000000000 main.cc 0000000000000000 l d .text.startup 0000000000000000 .text.startup 0000000000000000 g F .text.startup 000000000000001d main 0000000000000000 *UND* 0000000000000000 _Z5AliasPKv ... ``` ついでにSYMBOL TABLEを確認するとたしかに`_Z5AliasPKv`がいた。 次にLTOあり。 ``` $ objdump -x main.o ... Sections: Idx Name Size VMA LMA File off Algn 0 .text 00000000 0000000000000000 0000000000000000 00000040 2**0 CONTENTS, ALLOC, LOAD, READONLY, CODE 1 .data 00000000 0000000000000000 0000000000000000 00000040 2**0 CONTENTS, ALLOC, LOAD, DATA 2 .bss 00000000 0000000000000000 0000000000000000 00000040 2**0 ALLOC 3 .gnu.lto_.profile.b135ff34c05375c1 0000000f 0000000000000000 0000000000000000 00000040 2**0 CONTENTS, READONLY, EXCLUDE 4 .gnu.lto_.icf.b135ff34c05375c1 00000022 0000000000000000 0000000000000000 0000004f 2**0 CONTENTS, READONLY, EXCLUDE 5 .gnu.lto_.ipa_sra.b135ff34c05375c1 0000001c 0000000000000000 0000000000000000 00000071 2**0 CONTENTS, READONLY, EXCLUDE 6 .gnu.lto_.inline.b135ff34c05375c1 0000003e 0000000000000000 0000000000000000 0000008d 2**0 CONTENTS, READONLY, EXCLUDE 7 .gnu.lto_.jmpfuncs.b135ff34c05375c1 00000037 0000000000000000 0000000000000000 000000cb 2**0 CONTENTS, READONLY, EXCLUDE 8 .gnu.lto_.pureconst.b135ff34c05375c1 00000011 0000000000000000 0000000000000000 00000102 2**0 CONTENTS, READONLY, EXCLUDE 9 .gnu.lto_.ipa_modref.b135ff34c05375c1 00000015 0000000000000000 0000000000000000 00000113 2**0 CONTENTS, READONLY, EXCLUDE 10 .gnu.lto_.lto.b135ff34c05375c1 00000008 0000000000000000 0000000000000000 00000128 2**0 CONTENTS, READONLY, EXCLUDE 11 .gnu.lto_main.0.b135ff34c05375c1 00000121 0000000000000000 0000000000000000 00000130 2**0 CONTENTS, READONLY, EXCLUDE 12 .gnu.lto_.symbol_nodes.b135ff34c05375c1 0000004a 0000000000000000 0000000000000000 00000251 2**0 CONTENTS, READONLY, EXCLUDE 13 .gnu.lto_.refs.b135ff34c05375c1 0000000e 0000000000000000 0000000000000000 0000029b 2**0 CONTENTS, READONLY, EXCLUDE 14 .gnu.lto_.decls.b135ff34c05375c1 000002a2 0000000000000000 0000000000000000 000002a9 2**0 CONTENTS, READONLY, EXCLUDE 15 .gnu.lto_.symtab.b135ff34c05375c1 0000002f 0000000000000000 0000000000000000 0000054b 2**0 CONTENTS, READONLY, EXCLUDE 16 .gnu.lto_.ext_symtab.b135ff34c05375c1 00000005 0000000000000000 0000000000000000 0000057a 2**0 CONTENTS, READONLY, EXCLUDE 17 .gnu.lto_.opts 0000008a 0000000000000000 0000000000000000 0000057f 2**0 CONTENTS, READONLY, EXCLUDE 18 .comment 0000001f 0000000000000000 0000000000000000 00000609 2**0 CONTENTS, READONLY 19 .note.GNU-stack 00000000 0000000000000000 0000000000000000 00000628 2**0 CONTENTS, READONLY SYMBOL TABLE: 0000000000000000 l df *ABS* 0000000000000000 main.cc 0000000000000001 O *COM* 0000000000000001 __gnu_lto_slim ``` LTOありの方ではなんか `.gnu.lto_hogehoge`というどう見てもLTOに使われそうなセクションがいっぱいある。 GNUドキュメントの[LTO file sections](https://gcc.gnu.org/onlinedocs/gccint/LTO-object-file-layout.html)項目を見てみると、やはりこれらはLTOのための情報が書いてあるらしい。 例えば.gnu.lto_.symtabはELFのシンボルテーブルをreplaceしているらしい。 今回は各セクションの役割やどう使うかの詳細は追わないが、サイズの違いの理由はLTOに使用するための謎セクションがたくさんあることに起因するっぽい。 なので、LTOをしたい場合は実行ファイルを生成するときだけでなくオブジェクトファイルを生成するときから`-flto`のオプションをつけておかないと期待通りに動かない。 なお、最終的に生成される実行ファイル`a.out`については、Aliasなどがいなくなった分か少しだけ小さくなっている。 ## NOINLINE 最後にNOINLINE attributeをつけてみる。 [NOINLINE](https://source.chromium.org/chromium/chromium/src/+/main:base/compiler_specific.h;l=45;drc=f5bdc89c7395ed24f1b8d196a3bdd6232d5bf771)はChromium上でのマクロで、中身は`noinline`アトリビュート。 名前の通り、インライン展開しないでっていう指示をコンパイラに出している。 ```cpp // alias.cc __attribute__((noinline)) void Alias(const void* var) {} ``` alias.ccをこのようにupdateしてもう一度`-flto`つきでコンパイルしてみる。 ``` 0000000000001040 <main>: 1040: 31 c0 xor %eax,%eax 1042: c3 ret 1043: 66 2e 0f 1f 84 00 00 cs nopw 0x0(%rax,%rax,1) 104a: 00 00 00 104d: 0f 1f 00 nopl (%rax) ... 0000000000001140 <_Z5AliasPKv.isra.0>: 1140: c3 ret ``` Aliasシンボルは生きてるけどmainの中にはおらん。 ということはAliasに渡した引数も消えちゃう? base::debug::Aliasにはこんなコメントが書いてある。 ```cpp // This file/function should be excluded from LTO/LTCG to ensure that the // compiler can't see this function's implementation when compiling calls to it. NOINLINE void Alias(const void* var) {} ``` 今のままだとLTOで消えているように見える。残っていてほしいのはAliasではなく`var`として渡したものなので。 今回はChromiumとは違うコンパイラを使っているので、もしかしたらコンパイラ差かもしれない。あるいはどこかでLTOから外すようなコードがあるのかもしれない。 TODO(everyone): investigate