yunomuのブログ

趣味のこと

Haskellで特定のモジュールのコンパイルが遅すぎる

という話があって。

具体的にはaws-sdk(https://github.com/worksap-ate/aws-sdk)の開発中の話で、
aws-sdkではTravis CI(https://travis-ci.org/)を使ってビルドだけのテストをしているんですが、そのテストっていうかビルドが頻繁に失敗するようになってしまった。
原因はAWS.EC2.Typesのコンパイルで、このモジュールのコンパイルだけで5分以上かかる。その間何も画面出力などはされなくて、Travisでは何も出力がされないまま5分経過するとビルドプロセスが殺されてしまうという。

これはいけませんというか、いや別に普通にTravis以外ではビルドできてるからいいといやいいんだけど、これでTravisを切り捨てるのもかっこ悪いしねぇどうしようかと。
それとは別なのかもしれないけど32bit版LinuxだとAWS.EC2.Typesのコンパイル中にghcがsegmentation faultで落ちるという問題もあって、いい加減このモジュールどうにかした方がいいんじゃないかと思っていました。

当時のソースはこんな感じ。
aws-sdk/AWS/EC2/Types.hs at v0.9.2.1 · worksap-ate/aws-sdk · GitHub

データ定義が140個並んでいて、全部がEq, Show, Readのインスタンスになってて、あとその中でFromTextクラスのインスタンス化してる型が、TemplateHaskellを使うものが64個、TemplateHaskellを使わないものが8個という感じ。72個。

これ一見TemplateHaskellが重いのかなぁと思うんですけど、TemplateHaskellが絡むモジュールだけを分割しても大して変わらないし、モジュールを大雑把に2つに分けても結局その根本のAWS.EC2.Typesのコンパイル時間はあまりかわらないし、なんなんだこれと思って、exportするシンボルの数が多いとか最適化とかそういうのかなぁでもデータ定義しか無いし、

などとバカな話も考えはじめていたんですが、
との提案を受けたので、1日かけてもいいのでよろしくって言って彼にやってもらいました。

私が最初にやったテキトウな分割と違って、きちんとデータ間の依存関係を調べて分割してくれました。すげえ、ありがとうございます。
結果: aws-sdk/AWS/EC2/Types at f43a8b589db1271c9183339873a2115e61a7d9ce · worksap-ate/aws-sdk · GitHub
diff: Split EC2/Types. · f43a8b5 · worksap-ate/aws-sdk · GitHub

で、これをやった結果、ファイルが19個増えたものの、AWS.EC2.Typesのコンパイルが速くなるってもんじゃなくて、コンパイル時間が全体で60%ほど短くなりました。いや短くなりすぎだろうという気もしますが、短くなったものは仕方ないじゃないか。
ともあれ成果出てよかった。

よく考えてみるとHaskellのデータ構造ってC言語みたいな単純なメモリマップとは違って、コードに近いんですよね多分。いやghcが吐くコードがどんなものか知りませんけど。
まあそれが関係あるのかどうかはわかりませんけど、データ間の依存関係の計算に時間がかかってたんじゃないかなぁと。@amkkunが分割してくれた結果、ある程度の依存関係はモジュール分割の段階で解決してしまっているので、それで劇的に速くなったんじゃないかなぁ。同一モジュール間でデータの関係とか調べるのかな、そうだとしたら140個は多すぎるよな。
そういう感じでした。
そもそもexportが重いとかだったらAWS.EC2もかなり重くなってしかるべきなので、その予想はさすがに無かったかな。

ということで、シンボルが多すぎる時は気をつけましょうというか、適切にインタフェースを定義したモジュールに分割しましょうというお話でした。あとこれ関係で、できるだけexportするシンボルはきちんと列挙しておいた方がいいかもしれないとも思った。

32bit Linuxについては検証するの忘れてたからまた今度。