私はHaskell処理系であるGHCに趣味で貢献しています。詳しいことは過去の記事を見てください:
今回は、GHCのCIの問題を追求した話をします。前に「GHCデバッグ日誌」という記事を書いたので、今回は「CI編」としました。
full-ciが通らない
GHCのMerge Requestにラベル「full-ci」や「LLVM backend」がついていると、CIの際に追加のテストが走ります。それがここのところなぜか失敗するという状況で、私が書いたいくつかのMRのマージを阻害しています。
これらのラベルを外せばCIが通ってマージできると思いますが、あまり健全な解決策ではないので、CIの問題を解消して正面突破を試みよう、というのが今回の目的です。
jspaceの謎のタイムアウト
イシュー:
症状:特定のパイプライン(aarch64-linux-deb12-validate+llvm)の特定のテスト(jspace)がタイムアウトで失敗する。
再現性:CIの初回実行では毎回発生する。手元では再現しない。CIで使用されたバイナリーを手元で動かしても再現しない。失敗したパイプラインの再実行では発生しない。
これは手元で再現しないので結構厄介でした。原因は、推測ですが「jspaceがCPUコアをフルに使うので、CIのrunnerが複数のパイプラインを同時に実行する時に並列度が過剰になる」というものです。この推測が本当に正しいかはまだ確定じゃないのと、失敗したパイプラインを再実行すると通るようなので、修正の優先度は低いと思って放置しています。
i386でのLLVMバックエンドの問題
イシュー:
- #25498: Various SIMD tests fail in optllvm on i386 · Issues · Glasgow Haskell Compiler / GHC · GitLab(私の役割:原因究明)
- #25985: i386: Calling convention issue with SIMD vectors · Issues · Glasgow Haskell Compiler / GHC · GitLab(私の役割:上のissueの問題の一部を切り出して、原因究明)
i386 (x86-32) でSIMDプリミティブを使ったテストがセグフォしていました。バックエンドはLLVMです。
GHCで生成したバイナリーのセグフォの調査は辛いです。gdb/lldbでうまくバックトレースが取れないことがあります。tables next to codeというconfigurationをやめるとバックトレースが取りやすくなる気がする?まあ今回のケースはスタックが破壊されたのがバックトレースが取れない原因だったようですが。リバース実行ができるデバッガーを使うと良いのかもしれないですが、VM上のi386だとrrがうまく動いてくれなくて厳しいです。
結論を言うと、呼出規約周りに問題があるようでした。この結論に辿り着くためにどれだけの時間を使ったことか……。
別の問題として、後述するtest-primopsをi386ターゲットのLLVMバックエンドで回すとセグフォしました。これはすでにイシューが立っていました。
これも原因を調査しました。どうやら、末尾呼び出し最適化の強制に問題があるようです。
test-primops編
full-ciでGHC本体のテストが通ると、今度はtest-primopsというGHCとは別に管理されているテストが走ります。これの問題も調査しました。
- #8: Failure from pext on x86-64 · Issues · Glasgow Haskell Compiler / test-primops · GitLab(私の役割:原因の一部の指摘)
pextみたいなプリミティブの8ビット版、16ビット版が Word# -> Word# -> Word#
という型を持っているのに、test-primopsとLLVMバックエンドが Word8# -> Word8# -> Word8#
という型だと思って呼び出しているのが問題のようです。
- #26061: Sub-word arithmetic right shift with AArch64 NCG · Issues · Glasgow Haskell Compiler / GHC · GitLab(私の役割:報告と修正)
上記のテスト失敗現象を追求している間に、AArch64 NCGで8ビット整数、16ビット整数の算術右シフトが不正な結果を返すことがあることに気づきました。
- #13: bswap/W64 test fails on i386 · Issues · Glasgow Haskell Compiler / test-primops · GitLab
- #25601: LLVM backend miscompiles bswap64 on i386 · Issues · Glasgow Haskell Compiler / GHC · GitLab(私の役割:原因究明)
bswap64がi386で不正な結果を返す問題です。2番目のイシューはLLVMの問題として立っていますが、実際はNCGの問題のようです。LLVMバックエンドだとそもそもセグフォするので……。
x86 NCGの genByteSwap
関数で入出力レジスターが重なった場合を考慮できていないようです。
謎のセグフォの原因究明はしんどい
「Well-typed programs cannot go wrong」という言葉がありますが、コンパイラーがバグっているとちゃんとしたHaskellプログラムであっても普通にセグフォします。原因究明はgdbとかlldbとかを使うわけですが、デバッグ情報が皆無なので辛い。スタックトレースでシンボル名が出ると良い方です。せめてCmmのソースが出るようにならないのか。まあCmmのソースが出てもマクロとか特注組み込み関数とかを使っていると参考にはなりませんが。やはりアセンブリー言語を読まないとダメです。
リバース実行ができると良いのかもしれませんが、i386というマイナーな環境だとrrが動かなくて辛い。エスパー力が必要になる。
i386でだるい点を他にも挙げると、VS Code Remoteではi386の環境に入れない、というのがあります。あと、Hyper-VなVirtualBoxで動かすとなぜかAVXが使えない。
「やはり最後に頼るのは昔からのprintfデバッグね」というセリフもありましたが(幻聴)、デバッグ版のGHC RTSに +RTS -Dl -DL -DS
とかのオプションを与えると色々ログ出力が出てきて、役に立つかもしれないし、立たないかもしれません。GHCがセグフォする場合はGHC自体にデバッグ版RTSをリンクする必要があります。
謎のセグフォの原因究明はしんどいですが、それでも一部の構成要素はブラックボックスとして扱えるので楽なのかもしれません。例えば、CPUのエラッタには遭遇していません(対照:Ryzenが出た頃にCPUのエラッタで謎のセグフォが起こるという事案がありました)。例えば、LLVMのバグと呼べるものにはまだ遭遇していません。LLVMでのGHC向け呼び出し規約の定義が不完全なことによる問題はありましたが、LLVMのバグと呼べるかは怪しいです。例えば、GHC NCGのレジスター割り当て器は私にはブラックボックスですが、大抵の問題はレジスター割り当て器への入力が間違っていることに原因があります。
支援
私の活動を支援したい方は、今週末の技術書典で同人誌を買うという手段があります。Haskell関連の同人誌もあります。オンラインでも買えます。詳しくは記事を見てください:
他は、GitHub Sponsorsをやっています:https://github.com/sponsors/minoki 現在1名の方にスポンサーしていただいています。@toyboot4e さん、ありがとうございます!
あと「Binary Hacks Rebooted」という本も買っていただけると嬉しいです。gdbやrrの話なんかが載っています。
今後
こんな感じでCIの問題の原因究明をしていますが、問題が多すぎていつになったらfull-ciが通るようになるのかは不明です。気長にやっていきます。まあ、原因の見当はだいたいついたので、後はどの方針で、どの順番で修正していくか、という問題になるでしょう。
GHCのデバッグはしんどいですが、仕事とは別の種類のしんどさなので、ある種の気分転換になっています。