🐫

Perl でのバッチ処理に Parallel::Fork::BossWorkerAsync を使ったら処理速度が爆速になった話

2021/08/16に公開

1分台の処理が数秒で終わるだと……!

話の前提

私の Web サイトでは最近、 TF-IDF を利用したコサイン類似度っぽいものによる ある記事と似たような記事一覧を出す と言う機能を実装したのですが、Perl を使ってこの TF-IDF を云々して類似度スコアを算出する際、

なんか金の弾丸(ハイスペックマシン)を使っても処理し切るのに 1分ぐらい掛かるんだよなー……

となっていました。

それでその類似度スコアの算出に時間が掛かる問題に対処すべく、色々と CPAN Modules を物色していたのですが、その際に、

https://metacpan.org/pod/Parallel::Fork::BossWorkerAsync

を見つけて、それを使ってみたら見事に要件にマッチ、その結果として1分ぐらい掛かっていた処理が数秒台で完了できる様になったので、今回はその辺りの話を共有したいと思います。

Parallel::Fork::BossWorkerAsync とは?

Parallel::Fork::BossWorkerAsync とは Perl の CPAN Module の一つで、内部的には fork して子プロセスを作成し、その子プロセスに一定の並列処理をさせる、と言うライブラリです。

使い方としてはサンプルコードにもある通り、予め処理したいタスクの値を配列などで用意し、その配列の中のタスク値を fork で子プロセスを作りつつ並列処理する、と言う感じの流れになります:

use strict;
use warnings;

use Parallel::Fork::BossWorkerAsync;

sub process {
  my $val = shift;
  # ...
  return { result => $val };
}

sub main {
  my @values = @_;
  my  @tasks;
  for my $data (@values) {
    push @tasks, [ $data ];
  }
  
  
  my $bw = Parallel::Fork::BossWorkerAsync->new(
    work_handler => sub { process($_[0]->@*) },
    result_handler => sub { $_[0] },
    worker_count  => 15,
  );
  
  $bw->add_work(@tasks);
  while ( $bw->pending ) {
    my $ref = $bw->get_result;
    if ( $ref->{'ERROR'} ) {
      print STDERR $ref->{'ERROR'}, "\n";
    }
    else {
      print $ref->{'result'}, "\n";
    }
  }

  $bw->shut_down();
  
  exit 0;
}

main(@ARGV);

Parallel::Fork::BossWorkerAsync の利点と注意点

基本的に Parallel::Fork::BossWorkerAsync の並列処理は fork を使って子プロセスを作り、その中で時間の掛かる処理をさせる、と言う流れになっているため、大量の子プロセスを生成できる余裕があればあるほど、金の弾丸でバッチ処理を高速化できます

ただし注意点としては、fork を使って子プロセスを作っている構造上、グローバル変数は共有できても Read-only であるため、バッチ処理内で状態(いわゆる Global State) を持つ必要がある場合、なんらかの外部リソースで状態を保持しないと子プロセス間の状態の共有は出来ません。

また子プロセスの処理関数へ渡す引数のデータがデカいと、fork するための running cost が上がり過ぎて逆に処理速度が落るので、子プロセス全体で共有する値については、グローバル変数として定義しておく必要があると言えるかと思います。

よって上記の特性により Parallel::Fork::BossWorkerAsync に向く処理/環境と言うのは、

  1. 一つのタスクが自己完結して他のプロセスの情報を利用しない
  2. 一つずつの処理は数秒で終わるものの、実行するタスクが多い
  3. 並列実行するためのリソースが潤沢にある

と言う感じになるかと思います。

私の実際のユースケース

https://github.com/nyarla/the.kalaclista.com-v4-public/tree/develop/scripts

私が実際に Web サイトを作る際に使っている Perl scripts は上記の URL からアクセスできますが、私は Parallel::Fork::BossWorkerAsync を主に下記の用途で使っています:

  1. Text::MeCab を用いた本分テキストの tokenize の並列化
  2. 各記事データの TF-IDF 値の算出の並列化
  3. 実際に 似たような記事一覧 を算出するためのスコアリングの並列化
  4. WebSite Card (Zenn.dev などで見かけるアレ)のデータ取得の高速化

また私の場合だとバッチスクリプトの処理結果はファイルに YAML として書き出していて、それを YAML::XS で読み出して集計する、と言う方法で各プロセス処理の結果を取得しています。

なお私がこの記事の最初の方で述べた、

1分台の処理が数秒で終わるだと……!

と言う事実については、金の弾丸で殴ってる部分が多分にあるので(AMD Ryzen 3950 + 128GB メモリとか SSD)、実際に他の方の環境でここまで処理が高速化できるかどうか、についてはちょっと判断しかねる部分があります。

と言うかこの辺り本当に金の弾丸で殴ってますからね。流石に予算30万円で組んだデスクトップマシンは本当に強い。

以上

と言うことで今回の便利モジュールの紹介は以上です。

ただ今回の Parallel::Fork::BossWorkerAsync によるバッチ処理速度の向上、と言うのは、

  1. CPU 演算が関わる処理である
  2. State を保持するディスクの I/O 処理が早い
  3. fork して子プロセスを大量に作れるリソースがある

と言う条件下で最も効果を発揮する類いの事だと私は推察していて、これらの条件を満さない環境、例えば小規模な VPS や最低限の CPU 割り当ての Container Instance などでは、あまり実力を発揮できないのではないか、とも考えています。

また今回の紹介した処理方法についても、各処理が独立したバッチ処理であるからこそかなりの高速化が出来たのですが、これが並列化できないシーケンシャルなバッチ処理だと当然この方法は利用できません。

そのため Parallel::Fork::BossWorkerAsync によるバッチ処理の高速化について、 諸条件を満せば 実現可能であると言う点だけに注意すれば、あとはこの CPAN Module による高速化の恩恵を受けられるのではないか? と私は考えています。

Discussion