2014-04-30 [長年日記]
λ. IOモナドでヒープ割り当てを強制される件
Haskellでプログラムを書いていて、数年以上ずっと悩んでいる点として、
sumI :: UArray Int Int -> Int -> Int -> Int -> Int sumI a i end ret | i <= end = sumI a (i+1) end (ret + (a ! i)) | otherwise = ret
みたいな関数だと、UArray Int Int -> Int# -> Int# -> Int# -> Int# という型のワーカ関数が生成されるので、結果を即消費するような場合にはヒープ割り当てが発生しないのに対して、
sumM :: IOUArray Int Int -> Int -> Int -> Int -> IO Int
sumM a i end !ret
| i <= end = do
x <- readArray a i
sumM a (i+1) end (ret+x)
| otherwise =
return ret
みたいな関数だと、IOUArray Int Int -> Int# -> Int# -> Int# -> State# RealWorld -> (# State# RealWorld, Int# #) という型のワーカ関数ではなく、IOUArray Int Int -> Int# -> Int# -> Int# -> State# RealWorld -> (# State# RealWorld, Int #) という型のワーカーしか生成されず、Intが必ずヒープ割り当てされてしまう。
通常は大して問題にならないのだけど、凄まじい回数呼ばれる関数だと、それが積もり積もってプログラム全体のメモリ割り当ての数割を占めてしまったりして、そういう場合にはGCプレッシャーも馬鹿にならないので、出来ることなら1バイトたりともヒープ割り当てしたくないのである。 望む worker-wrapper transformation 結果を手で書いてしまえば良いのだけれど、GHC拡張に依存したコードを直書きするのも気が引けることもあって……

ご存知かもしれませんがJoachim Breitnerさんがこの問題に取り組んでますね https://ghc.haskell.org/trac/ghc/wiki/NestedCPR
おお。知らなかったです。ありがとうございます!