関数合成とオブジェクトサイズとパフォーマンスに関して

日曜日、第2回 スタートHaskell2 に参加してきました。

partake.in

一番印象的だったのが、Haskell をはじめて 1 ヶ月か 2 ヶ月ぐらいの方が、臆すことなく数十分の枠でプレゼンしていたことでした。


僕は Haskell は 2008 年からなので、えーと足掛け 4 年半ですか。それだけ経過してなお、実用的なプログラムを書いたことがないので、実力は横ばい。


スタート Haskell2 でも話題に挙がった、括弧を関数合成に置きなおすことについて、感想と調査結果をまとめておきます。

関数合成の妙技 - あどけない話

上記ブログエントリはスタート Haskell2 の主催者の方のもので、以下のようなことが書かれています。

foo p xs = sum (filter p (map (+1) xs))

上記は Haskell の初心者っぽいコードで、

foo p = sum . filter p . map (+1)

上記はそれを関数合成して Haskell っぽく直したもの、ということです。


僕の疑問は以下。

  • 慣れるといきなり最終形の Haskell っぽいコードがかけるのだろうか(わざわざ直すのは手間)
  • (.) は関数なので、Haskell っぽいコードの方が関数が多くなってしまっている(バイナリサイズや速度に影響はないのか)

後者に関しては検証が可能なので、やってみました。

module Foo1 (foo) where

foo p xs = sum (filter p (map (+1) xs))
module Foo1 (foo) where

foo p = sum . filter p . map (+1)

上記のソースコードを用意して、それぞれ Foo1.hs と Foo2.hs で保存し、以下のようにコンパイルします。

ghc -S Foo1.hs
ghc -c Foo1.hs
ghc -S Foo2.hs
ghc -c Foo2.hs

できたオブジェクトは、以下のようなサイズです。

2012/07/25  00:50             1,785 Foo1.o
2012/07/25  00:50             2,145 Foo2.o

関数を多く使っている分、サイズも増加しています。

アセンブラコードを比較してみると、オブジェクトサイズが増えている分、アセンブラコードも増えていました。

今回の話題とは全然関係ないですが、関数適用はそれぞれアセンブラ上で closure という名前がついているのが、いかにも Haskell から生成されたアセンブラっぽくて好印象。

さて、括弧を関数合成に置き換えると、オブジェクトサイズは大きくなり、体感できない程度にも速度が落ちていると思われます。関数合成を使用すると対応する閉じ括弧がそれ以降全体であるためなくなり、見通しがよくなるというのは完全に同意なんですが、そのためにサイズや速度(もしかするとサイズを犠牲に速度が向上している可能性もあるが)を犠牲にするのであれば、一概に置き換えるのが良し、ということになるのでしょうか。

2012/07/27 追記:

GHC を使用する場合は -O で最適化すべし、というアドバイスをコメント欄でいただきました。

以下のように最適化オプションを付けた場合:

ghc -O -S Foo1.hs
ghc -O -c Foo1.hs
ghc -O -S Foo2.hs
ghc -O -c Foo2.hs

できたオブジェクトは、以下のようなサイズになります。

2012/07/27  01:28             2,103 Foo1.o
2012/07/27  01:28             2,203 Foo2.o

どちらも最適化オプションをつける前と比べ、若干サイズが大きくなっていますが、それぞれの実装の差は縮まっています。

アセンブラは、A -> B というジャンプ命令が A -> X -> B という具合に、おそらく (.) 関数の処理が間に一つはさまっているようです(最適化した Foo1.s と Foo2.s の違いはほぼこれだけ)。

(.) を使って関数合成すると、GHC で最適化してコンパイルすると 20 命令弱のアセンブラになり、気にするほどでもないですが、関数合成に書き換えるのは、書き換え前と等価ではない、という理解になりました。