コンパイラのいじめかた / How to fight the compiler

A10e41b0a61d59f2258d7f6172c33479?s=47 kaityo256
November 20, 2019

コンパイラのいじめかた / How to fight the compiler

C++MIX #6

A10e41b0a61d59f2258d7f6172c33479?s=128

kaityo256

November 20, 2019
Tweet

Transcript

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

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

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

  4. 4

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

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

  7. 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; } } 上記は周期境界条件の補正コードで、頻繁に呼び出される
  8. 8 ある時、とあるスパコンで妙に遅いコードがあって調べてみたら、 関数の多段インライン展開がされていないのが原因だった そもそもコンパイラって何段まで インライン展開してくれるんだろう? これってトリビアになりませんか? このトリビアの種、つまりこういうことになります。 「コンパイラに多段呼び出しの関数を食わせた時、 力尽きるのはXX段」

  9. 9 #include<cstdio> 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
  10. 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
  11. 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万は時間がかかり過ぎた…)
  12. 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
  13. 13 こうしてこの世界に また一つ 新たなトリビアが生まれた こうしてこの世界に また一つ 新たなトリビアが生まれた インテルコンパイラは、関数のインライン展開を998段で力尽きる

  14. 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); これが全部合法&同じ結果に
  15. 15 アスタリスク、何個までつけられるんだろ?

  16. 16 #include <cstdio> int main() { ( ******************** ******************** ********************

    ******************** ******************** printf)("Hello World¥n"); } とりあえず200個つけてみた Hello World 問題なく実行できた
  17. 17 じゃあ1万個つけてみる def check(n) s = "*"*n f = open("test.cpp","w")

    f.puts <<EOS #include <cstdio> int main(){ (#{s}printf)("Hello World¥¥n"); } EOS f.close() return system("clang++ test.cpp") end check(ARGV[0].to_i) #include <cstdio> int main(){ (*...*printf)("Hello World¥n"); } アスタリスクたくさんつけて コンパイルするスクリプト
  18. 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で死んだ
  19. 19 printfにいくつアスタリスクつけると clangは死ぬんでしょうか? これってトリビアになりませんか? このトリビアの種、つまりこういうことになります。 「 clang++が死ぬのは、printfにアスタリスクをxxx個 つけた時」

  20. 20 def check(n) s = "*"*n f = open("test.cpp", "w")

    f.puts <<~EOS #include <cstdio> 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個が切れ目だったんですが……
  21. 21 こうしてこの世界に また一つ 新たなトリビアが生まれた こうしてこの世界に また一つ 新たなトリビアが生まれた printfに4281個アスタリスクをつけるとclang++が死ぬ ※ printfに2264個アスタリスクをつけるとインテルコンパイラも死ぬ

    ※ g++は100万個つけても大丈夫 補足トリビア
  22. 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; } ←ちなみにこんな関数
  23. 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になるかはわからない
  24. 24 #include <cstdio> 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 <https://github.com/Homebrew/homebrew-core/issues> for instructions. 419377回 ※ ggc-min-heapsizeを使い切ったのが原因。ガベージコレクションがらみっぽいが、詳細不明。
  25. 25 現代のコンパイラは恐ろしく賢い 「コンパイラに使われない」ために 人類は闘いを続けなければならない…