Slide 1

Slide 1 text

1 コンパイラのいじめかた 2019/11/20 C++ MIX #6 kaityo256

Slide 2

Slide 2 text

2 kaityo256 コンパイラいじめ芸人 主にスパコンで数値シミュレーションをする仕事 コンパイラいじめ

Slide 3

Slide 3 text

3 スーパーコンピューターでまともに動く並列言語が Fortran,C,C++ しかないから

Slide 4

Slide 4 text

4

Slide 5

Slide 5 text

5 コンパイラいじめとは、深化するAI に対する人類による知的エクスト リームスポーツである。人類の叡智 の集合体であり、近年は自意識さえ 持ちつつある「コンパイラ」に立場 を脅かされつつある人類。しかし、 ついに人類による反撃の狼煙が……

Slide 6

Slide 6 text

6 「お前、それコンパイラ開発者の前でも言えんの?」 と本社に拉致られてコンパイラ開発者の前でプレゼン することになる http://www.ssken.gr.jp/MAINSITE/download/wg_report/mcap/index.html SS研メニーコア時代の アプリ性能WG成果報告書 富士通C++コンパイラの最適化機能の改善について https://gist.github.com/kaityo256/328d51c27bf46605ddc0ec2acd1c2122

Slide 7

Slide 7 text

7 関数のインライン展開 最近のプログラマは、小さい関数はインライン展開されることを前提にコードを書く void adjust_boundary(double &x, double &y) { if (x < 0) { x += L; } else if (x > L) { x -= L; } if (y < 0) { y += L; } else if (y > L) { y -= L; } } 上記は周期境界条件の補正コードで、頻繁に呼び出される

Slide 8

Slide 8 text

8 ある時、とあるスパコンで妙に遅いコードがあって調べてみたら、 関数の多段インライン展開がされていないのが原因だった そもそもコンパイラって何段まで インライン展開してくれるんだろう? これってトリビアになりませんか? このトリビアの種、つまりこういうことになります。 「コンパイラに多段呼び出しの関数を食わせた時、 力尽きるのはXX段」

Slide 9

Slide 9 text

9 #include int func0(int a){ return a + 1; } int func1(int a){return func0(a);} int func2(int a){return func1(a);} int func3(int a){return func2(a);} int func4(int a){return func3(a);} int func5(int a){return func4(a);} int main(void){ int a = 0; printf("%d¥n",func5(a)); } 6段のインライン展開 main -> func5 -> func4 ->func3 -> func2 -> func1 -> func0

Slide 10

Slide 10 text

10 _main: pushq %rbp movq %rsp, %rbp leaq L_.str(%rip), %rdi movl $1, %esi xorl %eax, %eax callq _printf 6段のインライン展開 main -> func5 -> func4 ->func3 -> func2 -> func1 -> func0 clang++によるコンパイル結果 最後まで展開して則値を返している コンパイルオプション: g++ -O3 -S バージョン: Apple clang 11.0.0

Slide 11

Slide 11 text

11 main -> func999 -> ... -> func2 -> func1 -> func0 じゃあ千段は? movl $1, %esi xorl %eax, %eax callq _printf main -> func9999 -> ... -> func2 -> func1 -> func0 じゃあ1万段は? movl $1, %esi xorl %eax, %eax callq _printf main -> func99999 -> ... -> func2 -> func1 -> func0 じゃあ10万段は? movl $1, %esi xorl %eax, %eax callq _printf test.sが160万行 clang++すげー ※ GCCも1万段までは確認(10万は時間がかかり過ぎた…)

Slide 12

Slide 12 text

12 そういえばインテルコンパイラはどうだろう? まず6段から main -> func5 -> ... -> func1 -> func0 main -> func999 -> ... -> func2 -> func1 -> func0 千段 # func2(int) call _Z5func2i movl $.L_2__STRING.0, %edi movl %eax, %esi xorl %eax, %eax call printf movl $1, %esi orl $32832, (%rsp) xorl %eax, %eax ldmxcsr (%rsp) call printf 則値で返している あっ! コンパイルオプション: icpc -O3 -S バージョン: icc (ICC) 18.0.5 20180823

Slide 13

Slide 13 text

13 こうしてこの世界に また一つ 新たなトリビアが生まれた こうしてこの世界に また一つ 新たなトリビアが生まれた インテルコンパイラは、関数のインライン展開を998段で力尽きる

Slide 14

Slide 14 text

14 Qiitaでこんな記事を読んだ https://qiita.com/lo48576/items/92f1fc90643373d0b167 ( &printf)(" &printf = %p¥n", &printf); printf (" printf = %p¥n", printf); ( *printf)(" *printf = %p¥n", *printf); ( **printf)(" **printf = %p¥n", **printf); (***printf)("***printf = %p¥n", ***printf); これが全部合法&同じ結果に

Slide 15

Slide 15 text

15 アスタリスク、何個までつけられるんだろ?

Slide 16

Slide 16 text

16 #include int main() { ( ******************** ******************** ******************** ******************** ******************** printf)("Hello World¥n"); } とりあえず200個つけてみた Hello World 問題なく実行できた

Slide 17

Slide 17 text

17 じゃあ1万個つけてみる def check(n) s = "*"*n f = open("test.cpp","w") f.puts < int main(){ (#{s}printf)("Hello World¥¥n"); } EOS f.close() return system("clang++ test.cpp") end check(ARGV[0].to_i) #include int main(){ (*...*printf)("Hello World¥n"); } アスタリスクたくさんつけて コンパイルするスクリプト

Slide 18

Slide 18 text

18 $ ruby check.rb 10000 clang: error: unable to execute command: Illegal instruction: 4 clang: error: clang frontend command failed due to signal (use -v to see invocation) clangがSIGILLで死んだ

Slide 19

Slide 19 text

19 printfにいくつアスタリスクつけると clangは死ぬんでしょうか? これってトリビアになりませんか? このトリビアの種、つまりこういうことになります。 「 clang++が死ぬのは、printfにアスタリスクをxxx個 つけた時」

Slide 20

Slide 20 text

20 def check(n) s = "*"*n f = open("test.cpp", "w") f.puts <<~EOS #include int main(){ (#{s}printf)("Hello World¥¥n"); } EOS f.close system("clang++ test.cpp 2> /dev/null") end def binary_search s = 1 e = 10000 while (s!=e) && (s+1!=e) m = (s+e)/2 if check(m) puts "#{m} OK" s = m else puts "#{m} NG" e = m end end end binary_search Rubyで二分探索 5000 NG 2500 OK 3750 OK 4375 NG 4062 OK 4218 OK 4296 NG 4257 OK 4276 OK 4286 NG 4281 NG 4278 OK 4279 OK 4280 OK 4280個で死なず 4281個で死んだ ※ 以前調べた時には4285個が切れ目だったんですが……

Slide 21

Slide 21 text

21 こうしてこの世界に また一つ 新たなトリビアが生まれた こうしてこの世界に また一つ 新たなトリビアが生まれた printfに4281個アスタリスクをつけるとclang++が死ぬ ※ printfに2264個アスタリスクをつけるとインテルコンパイラも死ぬ ※ g++は100万個つけても大丈夫 補足トリビア

Slide 22

Slide 22 text

22 デバッガで追ってみる (Macは面倒なのでLinuxで) (gdb) r -cc1 (中略) c++ test-75d014.cpp [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib64/libthread_db.so.1". Program received signal SIGSEGV, Segmentation fault. 0x00000000009a8f60 in clang::Parser::ParseCastExpression(bool, bool, bool&, clang::Parser::TypeCastState) () lib/Parse/ParseExpr.cppのclang::Parser::ParseCastExpression という関数でSIGSEGVで死んだらしい ExprResult Parser::ParseCastExpression(bool isUnaryExpression, bool isAddressOfOperand, TypeCastState isTypeCast, bool isVectorLiteral) { bool NotCastExpr; ExprResult Res = ParseCastExpression(isUnaryExpression, isAddressOfOperand, NotCastExpr, isTypeCast, isVectorLiteral); if (NotCastExpr) Diag(Tok, diag::err_expected_expression); return Res; } ←ちなみにこんな関数

Slide 23

Slide 23 text

23 (gdb) bt #0 0x00000000009a8f60 in clang::Parser::ParseCastExpression(bool, bool, bool&, clang::Parser::TypeCastState) () #1 0x00000000009ab7bd in clang::Parser::ParseCastExpression(bool, bool, clang::Parser::TypeCastState) () #2 0x00000000009a9413 in clang::Parser::ParseCastExpression(bool, bool, bool&, clang::Parser::TypeCastState) () #3 0x00000000009ab7bd in clang::Parser::ParseCastExpression(bool, bool, clang::Parser::TypeCastState) () #4 0x00000000009a9413 in clang::Parser::ParseCastExpression(bool, bool, bool&, clang::Parser::TypeCastState) () #5 0x00000000009ab7bd in clang::Parser::ParseCastExpression(bool, bool, clang::Parser::TypeCastState) () #6 0x00000000009a9413 in clang::Parser::ParseCastExpression(bool, bool, bool&, clang::Parser::TypeCastState) () #7 0x00000000009ab7bd in clang::Parser::ParseCastExpression(bool, bool, clang::Parser::TypeCastState) () #8 0x00000000009a9413 in clang::Parser::ParseCastExpression(bool, bool, bool&, clang::Parser::TypeCastState) () #9 0x00000000009ab7bd in clang::Parser::ParseCastExpression(bool, bool, clang::Parser::TypeCastState) () #10 0x00000000009a9413 in clang::Parser::ParseCastExpression(bool, bool, bool&, clang::Parser::TypeCastState) () ... バックトレースを取ってみる 同じ関数を再帰的に呼び出して、スタック枯渇で死んだっぽい ※ MacでなぜSIGILLになるかはわからない

Slide 24

Slide 24 text

24 #include int main(void) { int i = 0; i++; i++; // ... i++; i++; printf("%d¥n", i); } 整数を419377回インクリメントするとMacのg++が死ぬ $ g++-9 test.cpp g++-9: internal compiler error: Segmentation fault: 11 signal terminated program cc1plus Please submit a full bug report, with preprocessed source if appropriate. See for instructions. 419377回 ※ ggc-min-heapsizeを使い切ったのが原因。ガベージコレクションがらみっぽいが、詳細不明。

Slide 25

Slide 25 text

25 現代のコンパイラは恐ろしく賢い 「コンパイラに使われない」ために 人類は闘いを続けなければならない…