Ractorで並列に動いているっぽい? - @ledsun blog の続きです。 Ractorで並列処理が出来そうなので、他の方法と性能を比べてみました。
比べるのは次の4つの方法です。
- シリアル処理
- スレッド
- プロセス
- Ractor
シリアル処理は比較用の逐次処理です。
あるActiveJobを実行して800ファイルを処理し終わる時間を比べます。 並列数は特に制限しません。 コア数は4つですが30並列ぐらい動くと思います。 必ずしも最適な並列化ではありません。
このジョブにはDBへのIOも含まれています。 並列化だけで速くならない時間もあります。 かなり雑に比べると、CPUバウンドな処理時間が1に対して、IOバウンドな処理時間は0.5です。 並列化でCPUバウンドな処理時間が半分になると、全体の処理時間は2/3になるイメージです。 理想的に4コアを使い切って、CPUバウンドな処理時間が1/4になると、全体の処理時間は1/2になります。 ここまでは行かないはずです。
予測
スレッドはスレッド切り替えのオーバーヘッドの分、シリアル処理より遅くなりそうです。 プロセスはGILを回避するので1.5倍くらいには速くなりそうです。 RactorもGILを回避出来ます。プロセスとどちらが早いか気になる所です。
結果
シリアル処理
Eapsed 2m 27sが基準になる値です。
スレッド
Eapsed 2m 27s。 シリアル処理より全く遅くなっていないのが意外です。
プロセス
Eapsed 1m 48s。 明確に速くなりました。
- 20220922 計測結果を間違えていたことに気がつきました。計り直して貼り直しています。。
Ractor
Eapsed 1m 52s。 明確に速くなりました。 1.3倍程度です。 1.5倍くらいになってくれると、並列化効いてる感がぐっと出てきて、うれしいです。
ソースコード
改変部分のみ抜粋したものをのせます。
シリアル処理
annotations_collection_with_doc.each do |annotations, doc| messages += Annotation.prepare_annotations!(annotations, doc, options) end
全く並列化せずに順次処理します。
スレッド
annotations_collection_with_doc.map do |annotations, doc| Thread.new do messages += Annotation.prepare_annotations!(annotations, doc, options) end end.each(&:join)
スレッドは変数は共有できるし、GILがあるのでレースコンディション考えなくて良いし、めちゃくちゃ簡単です。
プロセス
annotations_collection_with_doc = annotations_collection_with_doc.map do |annotations, doc| read, write = IO.pipe Process.fork do messages = Annotation.prepare_annotations!(annotations, doc, options) Marshal.dump([messages, annotations], write) end [read, doc] end.map do |read, doc| error_messages, annotations = Marshal.load(read) messages += error_messages [annotations, doc] end
プロセス間のデータのやりとりはparallel gemのソースコードを参考にしました。
データは特に工夫せずにMarshalしています。 これでプロセス間通信出来て、Rubyは便利です。 もしかするとMarshalが遅くなっているかもしれません。
Ractor
Ractor.make_shareable(TextAlignment::CHAR_MAPPING) Ractor.make_shareable(TextAlignment::LCSMin::PLACEHOLDER_CHAR) annotations_collection_with_doc.collect do |annotations, doc| r = Ractor.new do a, d, o = Ractor.receive m = Annotation.prepare_annotations!(a, d, o) Ractor.yield [m, a] end r.send [annotations, doc.dup, options] [r, doc] end.each do |r, doc| error_messages, annotations = r.take messages += error_messages [annotations, doc] end
Ractorはデータのやりとりのためのおまじないが色々必要です。
感想
わりとあっさり、各種の並行・並列処理が書けました。 Rubyは便利ですね。