RFPDFの高速化、StringIOを使う

RubyでPDFを生成したいと思い、RubyのPDFライブラリを物色していました。結果、プロジェクト管理ツールRedmineのプラグインとして開発されているRFPDFが、使いやすそう、かつ、日本語もある程度扱えるようだったので、使ってみることにしました。しかしながら速度が十分でなかったので、改造を施しパッチを作りました、というのが今回の話です。

flandre_tiled.png
クリックするとPDF

生成しようとしていたPDFは上のようなものです。今度の5月に開催されるMake: Tokyo Meeting 03に何かだせたらいいなぁ、ということでネタ出しをしている段階なのですが、それの検証用としして作成していたものです。ビットマップを読み込んで、モザイク画像を生成するスクリプト bmp_tiler.rb (後半に無理やり高速化をかけている痕跡がありますが、笑)で生成しました。円を沢山描きまくるスクリプトです。

このPDFを一枚完成させるのに、当初、およそ2分くらいかかっていました。どう考えても時間が掛かり過ぎておかしいと思ったので、チューニングをしてみることにしました。まずはどこがボトルネックとなっているのか、プロファイルを取ってみました。

ruby -rprofile bmp_tiler.rb

このようにすると、何の関数が何回呼び出されて、どれだけ時間がかかっているか一覧で表示されます。実行してみた結果、String#+(文字列の連結)が最も時間がかかっており、10万回以上の呼び出しで120秒以上消費していることがわかりました。かなり以前ですが、Rubyをはじめた頃に、K上さんという方からRubyのString#+は特に遅いので、StringIO#writeを代替に使うといいよ、ということを教えてもらったので、それを実践することにします。

結果、RFPDFでStringIOを使用するようにしたパッチrfpdf_stringio.patchを公開します。これを使うことで、同じ条件下で実行時間を10秒程度にまで短縮することががきました。如何にString#+が遅いかが実感できています。

簡単にString#+とStringIO#writeの違いを体感したければ、以下のスクリプトを実行してみるといいと思います。ノートPC(Core Duo L2400 @ 1.66GHz)で実験したみたところ、100000回呼び出しでString#+が14.39秒、StringIO#writeが1.24秒でした。

ruby -rprofile -e "a = String::new; 100000.times{|i| a += 'hoge'}"

ruby -rprofile -e "require 'stringio'; a = StringIO::new; 100000.times{|i| a.write('hoge')}; a.string"

このような話はRubyに限らず広くある話で、文字列の連結なら、JavaだとStringの代わりにStringBufferを使いましょう(もしかしたら今は最適化のお陰で効果はないかも?、Javaは久しく触っていません)とか、C++だとstringの代わりにstrstreamを使いましょう、といったことが知られていると思います。RubyのString#+が遅いのは、推測なのですが、GCが絡んでいる?とか、中でハッシュ値を計算しなおしている?(同一内容のStringはインスタンスが一つでシングルトンになっているんじゃなかったっけ?)あたりではないかと思います。もし正確なことをご存知の方がいましたら、つっこみお待ちしています(ソース読め、という話ですが、笑)。

March 19, 2009 10:57 fenrir が投稿 : 固定リンク | | このエントリーを含むはてなブックマーク

コメント

+=の代わりに<<を使うだけで結構速くなりますよ。

Posted by: foo : March 20, 2009 11:28 AM

>fooさん
情報ありがとうございます。本当ですね、String#<<使うとStringIO#writeと同程度の速度でますね。10万回の繰り返しで1.5秒程度でした。

Posted by: fenrir : March 20, 2009 01:04 PM

コメントする