Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

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

C++MIX #6

kaityo256

November 20, 2019
Tweet

More Decks by kaityo256

Other Decks in Programming

Transcript

  1. 4

  2. 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; } } 上記は周期境界条件の補正コードで、頻繁に呼び出される
  3. 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
  4. 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
  5. 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万は時間がかかり過ぎた…)
  6. 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
  7. 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); これが全部合法&同じ結果に
  8. 16 #include <cstdio> int main() { ( ******************** ******************** ********************

    ******************** ******************** printf)("Hello World¥n"); } とりあえず200個つけてみた Hello World 問題なく実行できた
  9. 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"); } アスタリスクたくさんつけて コンパイルするスクリプト
  10. 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で死んだ
  11. 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個が切れ目だったんですが……
  12. 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; } ←ちなみにこんな関数
  13. 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になるかはわからない
  14. 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を使い切ったのが原因。ガベージコレクションがらみっぽいが、詳細不明。