cerealのgetFloat*多用はメモリ長者にのみ許されし贅沢
約1年も書いてなかったのか.
ここ2,3年はそこそこのサイズの配列をどうこうする機会が多く,そういうとき特に何も考えなければ Data.Vector に突っ込んでおきたいキモチになる.で,実際に大きな配列をHaskellで扱うとメモリ不足でOOM Killerに殺されたりするわけだけど,そんな原因のひとつについて.
サンプルコードはココにあるのでヒマな人はどうぞ.
stack init stack build
まずputterというexecutableがそこそこ大きな要素数を持ったFloatの配列をlittle endianで出力する.
stack exec putter
これでカレントディレクトリにoutput.dat という50MBくらいのファイルができたかと.次にnaive-getterというexecutableでoutput.datを読み出す.やっていることは,Floatの配列に戻して総和を取り,元の配列の総和と一致するかを見ているだけ.
stack exec naive-getter
naive-getterの実装でByteStringからFloatのVectorに変換する部分は,次の通りcerealでgetFloat32leを使った脳直実装になっている.自然
runGetLazy (V.replicateM len getFloat32le) dat
しかし,コイツが想像以上にメモリをバカ食いするので,適当にプロファイルを見てみる.
stack build --profile stack exec -- naive-getter +RTS -hc -p -s stack exec -- hp2ps -c naive-getter.hp open naive-getter.ps
するとこんな感じになる.すごい.
8秒ちょいまではputterが出した配列をgetterでも再度作って総和とか評価してる部分なので無視するとして,Unboxed Vectorではないことを考慮しても以降のメモリ消費は立ち上がり過ぎている.どういうことかとgetFloat32leの実装を見に行くと,次のようになっている.
-- | Read a Float in little endian IEEE-754 format getFloat32le :: Get Float getFloat32le = wordToFloat <$> getWord32le (snip.) {-# INLINE wordToFloat #-} wordToFloat :: Word32 -> Float wordToFloat w = unsafeDupablePerformIO $ alloca $ \(ptr :: Ptr Word32) -> do poke ptr w peek (castPtr ptr)
1要素毎にコレやるわけだが,配列のようなコレクション中の要素を一気にたくさん変換するなら雑に次のような関数でいいかなと別の手段を用意する.
getFloat32leVector :: Int -> Get (V.Vector Float) getFloat32leVector n = do v <- V.replicateM n getWord32le return $ unsafeDupablePerformIO $ alloca $ \ptr -> do V.mapM (\x -> poke ptr x >> peek (castPtr ptr)) v
これでgetFloat32leを置き換えて次のように使ったrevised-getterを作り,
runGetLazy (getFloat32leVector len)
こちらでもnaive-getter同様にprofileを取る.
stack build --profile stack exec -- revised-getter +RTS -hc -p -s stack exec -- hp2ps -c revised-getter.hp open revised-getter.ps
結果はだいぶ減ったことがわかる.それでもまだ大富豪様ではあるわけだが
別に総和くらいだったらストリーム処理すばいいんだろうけどそれは簡単のために総和を置いてるだけで,現実的にはもっと配列全体をガチャガチャする処理が控えているためそういうわけにも.