ForEach-Object -Parallelが効果を発揮するパターン

前回はPowerShell 7.0.0 Preview 3に新しく追加されたForEach-Objectコマンドレットの並列処理機能(-Parallel)について紹介した。今回はこの機能の利点や欠点についてもうちょっと突っ込んで説明しておこう。

※ (編集部注)掲載時点の最新版はPowerShell 7.0.0 Preview4です。こちらは追ってご紹介します。

ForEach-Object -Parallelは、シンプルな記述が可能で、スクリプトブロックを並列処理する方法として強力な機能であることに間違いはないのだが、並列ごとに実行空間コンテキストの生成が行われるという重い処理が発生することになる。

そのため、この機能はパフォーマンス的に万能というものではなく、効果が発揮できるシーンと発揮できないシーンがはっきりとわかれている。

性能が発揮できる主なシーンは次の2つ。

マルチコアまたはマルチプロセッサの実行環境で、並列処理の対象となるスクリプトが長い時間をかけて計算を行う類のもの
ForEach-Object -Parallelが実行空間コンテキストを生成および初期化する処理が重いものの、それを超える長い時間処理が継続するようなものであれば、並列化することで得られるメリットの方が大きくなる。ただし、マルチコアまたはマルチプロセッサのマシンであるという前提が必要になる。シングルコアのマシンまたは実行環境でForEach-Object -Parallelを使うとまるで意味がなく、逆に遅くなる。また、-ThrottleLimitで指定する値はコアの総数まで。それ以上増やしても効果が得られないどころか、パフォーマンスが低下する可能性が高い

待機処理が発生するスクリプトを並列化するパターン
たとえば前回サンプルとして取り上げたコードにはsleepが含まれているが、これは待ちの処理であり、ForEach-Object -Parallelの効果が得やすい。ディスクI/Oも基本的には待ちの処理になるため効果が得やすい(高速なストレージなるほど効果が得にくくなる)。待ち時間が実行空間コンテキストの生成と初期化にかかる時間よりも長ければ効果が得られることになる。待ちの処理の場合にはシングルコアのマシンや環境でも効果を得ることが可能で、さらに-ThrottleLimitで指定する値はコアの数を超えて指定しても効果が得られる。どの程度の数を指定するかは環境によって異なるため、実際に実行してもっともパフォーマンスが高くなる値を指定するようにすればよい

カーネル側の動作に明るい方は、この動きも理解しやすいだろう。ForEach-Object -Parallelを使用する場合、並列するときに必ず重い処理が発生するということを覚えておいてほしい。その処理を含めても高速化が可能かどうかが、この機能を利用するかどうかのひとつの基準になる。

ForEach-Object -Parallelを使ってはいけないパターン

ForEach-Object -Parallelは並列化ごとに実行空間コンテキストの生成と初期化を行うため、スクリプトブロックの中の処理が短く、実行空間コンテキストの生成&初期化処理よりも短い場合、パフォーマンスが大きく悪化する。すぐに終わるような処理で-Parallelを使用すると逆にすごく遅くなるということだ。

次のスクリーンショットはそのサンプルだ。10,000回処理を行っているのだが、最初のスクリーンショットは並列化しないで、2つ目のスクリーンショットでは5並列で処理を行っている。

10,000回処理を実施

10,000回行う処理を5並列で実施

結果は次のとおりだ。

  • 並列なし: 0.87秒
  • 並列あり: 204.141秒


並列化した方が230倍ほど遅くなっている。このように、軽めの処理はForEach-Object -Parallelで並列化すると性能が悪化する。

ForEach-Object -Parallelで並列化するときは、高速化が可能だという確信があるとき、ということになるだろう。高速になるかどうかわからないときは、とりあえず実行して時間を測定してみることをお薦めしたい。それほど変わらないならやらない方がよいだろう。

並列とスレッド間通信

ForEach-Object -Parallelで並列処理ごとに実行空間コンテキストをコピー…というか生成しているのは、PowerShell Coreの規約に準拠するためだ。PowerShell Coreの仕様を満たしつつ、並列処理を実現するには、実行空間コンテキストを生成してそこで処理を走らせる必要がある。今後高速化の余地はあるとは思うが、現在の実装だと生成時の負荷に対してそこまで劇的な高速化は見込めないだろう。

また、実行空間コンテキストはスクリプトを実行するためのひとつの分離単位になっており、基本的にこれらコンテキスト間で通信を行ったりデータを共有することはできない仕組みになっている。ただし、$usingを使えばデータ共有じみたことはできてしまうものの、その方法はスレッドセーフではないので、安全に処理を行うことはできないとされている。

UNIXシェルであればプロセス間通信を使うことで同時に動作するプロセス間でデータをやり取りすることができる。名前付きパイプやファイルシステムを使うというのもひとつの方法だ。

PowerShell Coreの並列機能はまだそこまで簡単に並列処理を実現できるというものではないようだ。しかし、これはあくまでも最初の段階だ。今後登場する機能で並列処理に関してはさらに便利な機能が追加される可能性もある。今後のリリースに注目しておきたいところだ。

参考資料