早速で恐縮なんですが,現在スタンドアローンのWin7環境でHaskellPlat...

早速で恐縮なんですが,現在スタンドアローンのWin7環境でHaskellPlatform(GHC 8.4.3)を使って作業をしています.csvデータを取り込みたく,以下のパーサーをReal World Haskell 等見ながら作成してみました.数100mのファイルなら問題なく動くのですが,4Gくらいになるとメモリヒープを起こします.マシンのメモリは128積んであるので,読めないことはないと思うのですが,理由がわかりません.ご知見ありましたら,ご教示いただければ幸いです.

Replies

早速で恐縮なんですが,現在スタンドアローンのWin7環境でHaskellPlatform(GHC 8.4.3)を使って作業をしています.csvデータを取り込みたく,以下のパーサーをReal World Haskell 等見ながら作成してみました.数100mのファイルなら問題なく動くのですが,4Gくらいになるとメモリヒープを起こします.マシンのメモリは128積んであるので,読めないことはないと思うのですが,理由がわかりません.ご知見ありましたら,ご教示いただければ幸いです.

GHC で吐いたプログラムのヒープサイズはデフォルトで無制限か
https://downloads.haskell.org/~ghc/8.4.3/docs/html/users_guide/runtime_control.html#rts-flag--M%20%E2%9F%A8size%E2%9F%A9

> 4Gくらいになるとメモリヒープを起こします

メモリ不足でOSによってkillされるってことですか?

128gbまで使い切ってキルされるか自分で強制終了しています。色々とstrictにしてみたりはしたんですが変わらず、悩んでいます。

ヒーププロファイリングをしてみる、とかですかねぇ。
ちなみに、CSVパーサーと言えばcassavaが有名ですが、それでやるのはダメなんですかね。

プロファイリングをしてみて動くサイズのファイルでは問題なさそうなのですが、一定サイズを超えるとプロファイルはくまえに死んでしまって困っています。既存のものを入れられると助かるんですがオフラインで依存環境が解決できずに車輪の再発明をしている次第です。

なので、プロファイリングして問題のないサイズの入力でたくさんアロケートしている関数を探すのです。

... それはさておき、ざっと読んだ感じ、単純に Data.TextをData.Text.LazyにしてStrictにするのを止めるだけで大きく変わりそうな気がします。

メモリリークは結構、呼ぶ側のコードの影響もあるかもしれません。

問題のないサイズで元々Lazyなものを使って試していたんですが、Strictの方がスコアが良かったので、こちらにしていました。Lazy vesionから順にbang pattern を付けて見たんですが、最終的にこれが一番結果が良かったです。cellの部分でdeepseqも試しましたがそちらは悪化したのでやめました。

readCSVTWin path >>= mapM_ (mapM_ Text.putStrLn)) みたいに即座に消費するコードを書いてみてリークしないようなら、呼ぶ側の原因かも

呼ぶ側では、単純に結果のheadのlengthを評価するだけでテストしていたんですが、単純に出力するだけのものを試してみます。

あ、これ戻り値IOだから遅延しないか

いやparseCSVTはIOじゃないから遅延しますね

や、パース失敗かどうか最後まで入力読まないと駄目だから遅延しないです。だとmapM_でも遅延にならないですね

ちょっと混乱したので見なかった事に…

行の分割を lines 関数に任せて1行ごとにパーサーを走らせれば回避できる?

プロファイルの先頭部分ですが,quated Cell が悪さをしているらしくいじってみましたがどこが原因がなかなかわかりません.

lines関数も試させていただきます, 環境がネットに繋がっていないので(現在自前にうつしています)反応遅くすみません.

lines関数で行ごとに区切って読む方が軽いのは間違いなさそうですね。そうすると、遅延IOにより、最初の要素にアクセスしただけでは最終行まで読まないので注意です

strict 拡張をしていても遅延が起きるでしょうか?

おそらく起きます。Strict拡張やBangパターンはあくまでseq相当で、WHNFまでしか評価しないので。deepseqすれば最後まで行くはずです

問題は *なにを* deepseqするかなんですが…🤔

ありがとうございます. 今,putStrLnで試していますが,linesをその後に試させていただきます.
少し話題がずれるのですが,1GB程度のファイルをText.IO.readFilesしただけでも5~6GB消費するのは,Boxedだとしかたないのでしょうか?

readCSVTWin path >>= \x -> deepseq x (return ())) とかですかね>何を~

total allocだからreallocして領域広げた分を累計してるのかも(未確認)

すみません,計算待ちです,6GBのファイルを読ませてみていますが,現状121GB使っているので,呼ぶ側ではなさそうです.

conduitなどを使う方がいいでしょうね。

不勉強でconduitをあまり理解していないのですが,可能なことは逐次処理で行別にパースしていくイメージであっているでしょうか?

行単位でもできますし、調整すればセル単位でもできます

現在linesで先に分割したものをためしてみているます.評価をputSTRLnにしたままだったので,出力に時間をくっていますが,40GB程度の消費で終わりそうです.

一応,当面の課題は解決しました.ありがとうございます.一点疑問なのですが,6GBのcsvを[[Text]]で保持して,40GBほどメモリを消費するというのはHaskellの相場からすると通常なのでしょうか?conduit等も試させていただきますが,メモリ的にも節約が見込めるものでしょうか?

- Total allocなので、allocしてすぐ解放したような物もカウントされているのでは?
- HaskellというかParsecが遅い

あたりも考えられるので、詳しくは検証しないと分からない所です。メモリ使用量はともかく速度については、リークや文字列処理に気を遣って書いたHaskellはC++の5倍程度遅く、JVMロード時間を除いたJavaとトントンくらい、とされています。

Total allocよりも、Total memory in useを見るべきか。あとはグラフ化した奴も見れば色々分かるかもしれません

メモリに関しては載せていませんでしたが(すみません)単純にタスクマネージャーで見ていたの実行時の消費です。速度に関しては、並列化でどうにか頑張れると思うのですが、メモリ消費を減らす知見があれば教えていただけると大変助かります。要求ばかりですみません。

ただ、認識不足でしたが一度に読まずにバッチ処理をしろという意味だと思いますので(>>conduit )そちらで努力してみます。

ヒーププロファイリングのグラフ化は行ったことがなかったので今後利用させていただきます。ご教示助かります。ありがとうございました。

conduitの敷居が高ければ、Lazy TextでgetContentsする->Lazyのlinesで分割->toStrict->パース でも一応大丈夫ですね。あるいは他言語同様にgetLineでループして頑張るか

なるほど。遅延評価の活かし方参考になります。せっかくなのでstreamも勉強してみますが、当面の実装はそちらで試してみます。使い始めたものの全く扱えていないので大変勉強になります。

データの使われ方によって対策が変わると思います。前から順にストリーム処理可能ならcassavaなどのstreamingインタフェースを使えば良さそうですが、全てのデータをメモリ上に保持してランダムアクセスする場合は[[Text]]ではなくもっとコンパクトなデータに変換するのが良いと思います。データが数値なら適切な数値型にしてunboxed vectorにするなどです。

リストはかなり富豪的なデータ構造で大量の要素を保持しなければならない時にはメモリ使用量やGCの仕事量が増えて遅くなりがちなので気をつけないといけません

ご回答ありがとうございます.日本語混じりの文字列なので,数値型やBytestringなどは使えません.Unboxed vectorは,そのままTextを要素にできないように理解していましたが,
data NewText = NewText {text :: {-# Unpack #-} Text のようなものをBoxedVectorに入れても,同じ効果が得られるものでしょうか.

newtype にはUnpackプラグマは使えないのですね..修正しました

それと気になるのが,Parserのような処理で返り値をVectorにするとConsを各Char(をT.pack)したものに繰り返すことになりそうですが,そこが少し怖いです.以前,手当たり次第にVectorにしていて,snoc,cons,++あたりを繰り返して死んだことがあり,寧ろリストの方が安全ではないかと考えていました. いずれにしても手元で試させていただきます.

勉強のために色々試してみたのですが、以下のように変更したら total memory in use が1/3ぐらいになりました。

cell = (quotedCell <|> many (noneOf ",\n\r")) >>= (\res -> return $! T.pack res)

これ意識して避けるの絶対無理ですね…… Textでゴリゴリやりたい人はattoparsec使えって事なんでしょうか

megaparsecとかtrifectaならまた違ったりするのかな。 🤔

そもそもparsecはincremental parsingに対応していなかったと思うので、大きなデータを使う場合はattoparsecを使ってくださいということだと思います

megaparsec でも書き直してみましたが、同様でした。attoparsec なら違うかもしれないですね。

attoparsecは"Use the Text-oriented parsers whenever possible" に従えば大丈夫そう? http://hackage.haskell.org/package/attoparsec-0.13.2.2/docs/Data-Attoparsec-Text.html

あと >>= で書いてある部分を do で書き換えて、全部の関数に型を明示的に書いたら、処理的には何も変更していませんが 50MB ぐらい減りましたね。

Strict拡張があるので、doで変数束縛すると暗黙にseqが掛かるからそれじゃないですかね

確かに手元で確認したら Strict 拡張の有無でプロファイル結果が変わったので、それっぽいです。

ここまでくると途中経過のソース全部含めてBIG MOONの記事になっていて欲しくなりますね 🙏

書いてみますね!

@UEC0PN1PA@U5B3EGEFQ さんのブログに載せちゃっても大丈夫でしょうか。。。?

@U4LGTMTMK 問題ありません。皆さん色々議論してくださって大変勉強になりましたので是非残してください。