@ledsun blog

無味の味は佳境に入らざればすなわち知れず

Ruby並行・並列くらべ

Ractorで並列に動いているっぽい? - @ledsun blog の続きです。 Ractorで並列処理が出来そうなので、他の方法と性能を比べてみました。

比べるのは次の4つの方法です。

  1. シリアル処理
  2. スレッド
  3. プロセス
  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

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は便利ですね。

参考