GHCでコンパイルしたバイナリを小さくする方法
概要
HaskellはGHCでコンパイルしてバイナリを生成することができる。しかし、Cの時と比べて意味不明なレベルでバイナリが大きくなる。原因と対策を書いておく。
環境
- OS
$ uname -a Linux ubuntu 3.13.0-63-generic #103-Ubuntu i686
$ ghc --version The Glorious Glasgow Haskell Compilation System, version 7.6.3
$ gcc --version gcc (Ubuntu 4.8.4-2ubuntu1~14.04) 4.8.4 ...
ソース
-- hello.hs main = putStrLn "Hello, World!" -- E.O.F.
/* hello.c */ #include <stdio.h> int main(void) { printf("Hello, World!\n"); return 0; } /* E.O.F. */
どちらも "Hello, World!"と出力するだけの簡単なプログラム。
コンパイル
サイズ
$ ll ?_hello 7332 c_hello 839047 h_hello
HaskellはCの100倍以上の大きさを持っている。
最初見たときは叫んだ。
原因
GHCはデフォルトでHaskell runtime(libHSrts.a)を静的リンクするようだ。
こいつが丸々バイナリに組み込まれてしまうのが問題みたいだった。
GCCは最初から動的リンクするイケメン。
対策
動的リンクに切り替えてやればよい。
UbuntuのHaskell platformには動的リンクが含まれていない。
だから最初に ghc-dynamic をインストールする。
$ apt-get install ghc-dynamic
それから以下を実行。
$ ghc -dynamic -o h_hello hello.hs` $ ll ?_hello 7332 c_hello 16397 h_hello
2倍程度まで小さくなった。 デバッグ情報を消去するともう少し小さくなる。参考までにデバッグ情報をなくしたCバイナリ(sc_hello)も載せておく。
$ strip -p --strip-unneeded --remove-section=.comment -o sh_hello h_hello $ ll *_hello 7332 c_hello 16397 h_hello 5448 sc_hello 9688 sh_hello
c_helloのほぼ同程度のサイズまで落とせた。
結論
Haskellのバイナリはリンクさえどうにかすればサイズは2倍程度にできることがわかった。
おまけ
このままCバイナリに負けたままでは悔しかったからもう少し頑張ってみた。
実行形式ファイルを実行可能な圧縮ファイルに変換するgzexe
というコマンドがある。これは実行ファイルをgzipで圧縮し、圧縮したファイルの先頭に解凍するスクリプトを埋め込む。容量の小さいディスクにおいて有効なコマンドである。
$ gzexe h_hello h_hello: 74.2% $ ll *_hello 7332 c_hello 5061 h_hello <- 圧縮後 16397 h_hello~ <- 圧縮前
・・・ついにHaskellバイナリはCバイナリを越えた!
$ gzexe c_hello c_hello 68.7% $ ll *_hello 3132 c_hello <- 圧縮後 7332 c_hello~ 5061 h_hello <- 圧縮後 16397 h_hello~
という夢を見た。おしまい。
メモ
最初にPreludeの関数をputStrLn以外をhidingしてみたが、サイズ面では全く変化がなかった。
関数のロード関連はサイズに関係しないフラグなどがあるのだろうか。
参考リンク
haskell - Could not find module Prelude... dyn libraries for package base? - Stack Overflow