@ledsun blog

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

並列数をかえても処理時間が変わらない謎

とあるRuby on RailsアプリケーションのSidekiqで動くバックグラウンドジョブを高速化をしています。 CPUバウンドな処理に時間が掛かっています。 並列化して高速化できるか試しています。 RactorでWoker pool - @ledsun blog で、4コアのPCで並列化できるところまで確認できています。 ただし、4並列というほどには高速化出来ていませんでした。 検証につかっているPCやWSLなどの環境に起因するものでしょうか? 検証用にAWS EC2を借りて環境構築しました。

8コアあるt3.x2largeを使います。 今回は使って4並列と8並列で計測して、高速化できるか試します。

4並列で計測

つぎのようにRactorを使って4ワーカーで実行するソースコードがあります。 https://github.com/pubannotation/pubannotation/blob/54a53d80d39a521eff3f4be4110b844f075f4d26/app/models/project.rb#L760-L772

    workers = (1..4).map do
      Ractor.new pipe do |pipe|
        while msg = pipe.take
          a, ref_text, o = msg
          aligner = TextAlignment::TextAlignment.new(ref_text, o)
          m = a.map do |annotation|
            Annotation.align_annotations!(annotation, ref_text, aligner)
          end.flatten

          Ractor.yield [m, a]
        end
      end
    end

計測してみましょう。

4並列で819文章の処理に2分16秒掛かります。

8並列で計測

これに次のパッチをあてて8並列します。

diff --git a/app/models/project.rb b/app/models/project.rb
index 9a769ef2..85da2cac 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -757,7 +757,7 @@ class Project < ActiveRecord::Base
       end
     end

-    workers = (1..4).map do
+    workers = (1..8).map do
       Ractor.new pipe do |pipe|
         while msg = pipe.take
           a, ref_text, o = msg
@@ -826,7 +826,7 @@ class Project < ActiveRecord::Base

     messages << { body: "Uploading for #{num_skipped} documents were skipped due to existing annotations." } if num_skipped > 0

-    InstantiateAndSaveAnnotationsCollection.call(self, aligned_collection) if aligned_collection.present?
+    #InstantiateAndSaveAnnotationsCollection.call(self, aligned_collection) if aligned_collection.present?

     messages
   end

パッチの後半はDBへのインサート処理をコメントアウトしています。 これは4並列のときも行っています。

計測してみます。

8並列で819文章の処理に2分16秒掛かります。

並列数をあげたのに処理時間は変わりません。

そもそも並列化できていないのでしょうか?

直列で計測

並列化しないと次のようなソースコードです。

https://github.com/pubannotation/pubannotation/blob/15ba8c0a546a3447845af19006026d67f906f263/app/models/project.rb#L752-L758

    annotations_collection_with_doc.each do |annotations, doc|
      ref_text = doc.original_body.nil? ? doc.body : doc.original_body
      aligner = TextAlignment::TextAlignment.new(ref_text, options)
      messages += annotations.map do |annotation|
        Annotation.align_annotations!(annotation, ref_text, aligner)
      end.flatten
    end

これで計測してみましょう。

直列で819文章の処理に3分13秒掛かります。

一分近く処理時間が伸びます。 並列化できてそうです。

考察

8並列と4並列の処理時間が全く一緒なのが予想外の結果です。 2倍にならないとしても、いくらか高速化することを予想していました。 これはどのような現象が起きているのでしょうか?いくつか仮説を考えてみます。

「並列化した先で実は同一の資源を使っている部分がある。」 今回Ractorで並列化しているので、メモリやDBへの接続を共有したらエラーがおきるはずです。

「並列処理が終わったあとにボトルネックがある。」 stackprofで実際に掛かっている時間を計測すれば、起きているかどうか確認できそうです。 Stackprofで時間の掛かる処理を探そうとして上手く行かなかった話 - @ledsun blog で、直列処理を計測したときは、並列化した部分に時間が掛かっていました。 この線もなさそうです。

「8並列の遙か手前、たとえば3並列でサチっている。」 原因は推測できませんが、起きているかもしれません。 これは2並列、3並列で計測すれば、起きているかどうか確認できそうです。