次の方法で共有


並列実行を使用してパフォーマンスを最適化する

PowerShell には、並列呼び出しを作成するためのオプションがいくつか用意されています。

  • Start-Job は、PowerShell の新しいインスタンスを持つ個別のプロセスで各ジョブを実行します。 多くの場合、線形ループの方が高速です。 また、シリアル化と逆シリアル化により、返されるオブジェクトの有用性が制限される可能性があります。 このコマンドは、すべてのバージョンの PowerShell に組み込まれています。
  • Start-ThreadJobThreadJob モジュールで見つかったコマンドレットです。 このコマンドでは、PowerShell 実行空間を使用して、スレッド ベースのジョブを作成および管理します。 これらのジョブは、 Start-Job によって作成されたジョブよりも軽量であり、プロセス間のシリアル化と逆シリアル化に必要な型の忠実性が失われる可能性を回避します。 ThreadJob モジュールには、PowerShell 7 以降が付属しています。 Windows PowerShell 5.1 の場合は、PowerShell ギャラリーからこのモジュールをインストールできます。
  • PowerShell SDK の System.Management.Automation.Runspaces 名前空間を使用して、独自の並列ロジックを作成します。 ForEach-Object -ParallelStart-ThreadJobの両方で、PowerShell 実行空間を使用してコードを並列で実行します。
  • ワークフローは、Windows PowerShell 5.1 の機能です。 ワークフローは、PowerShell 7.0 以降では使用できません。 ワークフローは、並列で実行できる特殊な種類の PowerShell スクリプトです。 これらは実行時間の長いタスク用に設計されており、一時停止および再開できます。 ワークフローは、新しい開発には推奨されません。 詳細については、about_Workflowsを参照してください。
  • ForEach-Object -Parallel は PowerShell 7.0 以降の機能です。 Start-ThreadJobと同様に、PowerShell 実行空間を使用してスレッド ベースのジョブを作成および管理します。 このコマンドは、パイプラインで使用するように設計されています。

実行のコンカレンシーを制限する

スクリプトを並列で実行しても、パフォーマンスの向上は保証されません。 たとえば、次のシナリオでは、並列実行の利点があります。

  • マルチスレッド のマルチコア プロセッサでのコンピューティング集中型スクリプト
  • 結果の待機またはファイル操作の実行に時間を費やすスクリプトは、それらの操作によって互いにブロックされない限りです。

並列実行のオーバーヘッドと、実行される作業の種類のバランスを取る必要があります。 また、並列で実行できる呼び出しの数にも制限があります。

Start-ThreadJobコマンドとForEach-Object -Parallel コマンドには、一度に実行されるジョブの数を制限する ThrottleLimit パラメーターがあります。 より多くのジョブが開始されると、キューに登録され、現在のジョブ数がスロットル制限を下回るまで待機します。 PowerShell 7.1 の時点で、 ForEach-Object -Parallel は既定で実行空間プールからの実行空間を再利用します。 ThrottleLimit パラメーターは、実行空間プールのサイズを設定します。 既定の実行空間プール のサイズは 5 です。 UseNewRunspace スイッチを使用して、イテレーションごとに新しい実行空間を作成できます。

Start-Job コマンドには ThrottleLimit パラメーターがありません。 一度に実行されるジョブの数を管理する必要があります。

パフォーマンスを測定する

次の関数 Measure-Parallelは、次の並列実行アプローチの速度を比較します。

  • Start-Job - バックグラウンドで子 PowerShell プロセスを作成する

  • Start-ThreadJob - 各ジョブを個別のスレッドで実行します

  • ForEach-Object -Parallel - 各ジョブを個別のスレッドで実行します

  • Start-Process - 外部プログラムを非同期的に呼び出します

    この方法は、PowerShell コードのブロックを実行するのではなく、並列タスクが外部プログラムへの 1 回の呼び出しのみで構成されている場合にのみ有効です。 また、この方法で出力をキャプチャする唯一の方法は、ファイルにリダイレクトすることです。

function Measure-Parallel {
    [CmdletBinding()]
    param(
        [ValidateRange(2, 2147483647)]
        [int] $BatchSize = 5,

        [ValidateSet('Job', 'ThreadJob', 'Process', 'ForEachParallel', 'All')]
        [string[]] $Approach,

        # pass a higher count to run multiple batches
        [ValidateRange(2, 2147483647)]
        [int] $JobCount = $BatchSize
    )

    $noForEachParallel = $PSVersionTable.PSVersion.Major -lt 7
    $noStartThreadJob = -not (Get-Command -ErrorAction Ignore Start-ThreadJob)

    # Translate the approach arguments into their corresponding hashtable keys (see below).
    if ('All' -eq $Approach) { $Approach = 'Job', 'ThreadJob', 'Process', 'ForEachParallel' }
    $approaches = $Approach.ForEach({
            if ($_ -eq 'ForEachParallel') { 'ForEach-Object -Parallel' }
            else { $_ -replace '^', 'Start-' }
        })

    if ($noStartThreadJob) {
        if ($interactive -or $approaches -contains 'Start-ThreadJob') {
            Write-Warning "Start-ThreadJob is not installed, omitting its test."
            $approaches = $approaches.Where({ $_ -ne 'Start-ThreadJob' })
        }
    }
    if ($noForEachParallel) {
        if ($interactive -or $approaches -contains 'ForEach-Object -Parallel') {
            Write-Warning 'ForEach-Object -Parallel require PowerShell v7+, omitting its test.'
            $approaches = $approaches.Where({ $_ -ne 'ForEach-Object -Parallel' })
        }
    }

    # Simulated input: Create 'f0.zip', 'f1'.zip', ... file names.
    $zipFiles = 0..($JobCount - 1) -replace '^', 'f' -replace '$', '.zip'

    # Sample executables to run - here, the native shell is called to simply
    # echo the argument given.
    $exe = if ($env:OS -eq 'Windows_NT') { 'cmd.exe' } else { 'sh' }

    # The list of its arguments *as a single string* - use '{0}' as the placeholder
    # for where the input object should go.
    $exeArgList = if ($env:OS -eq 'Windows_NT') {
        '/c "echo {0} > NUL:"'
     } else {
        '-c "echo {0} > /dev/null"'
    }

    # A hashtable with script blocks that implement the 3 approaches to parallelism.
    $approachImpl = [ordered] @{}

    # child-process-based job
    $approachImpl['Start-Job'] = {
        param([array] $batch)
        $batch |
            ForEach-Object {
                Start-Job {
                    Invoke-Expression ($using:exe + ' ' + ($using:exeArgList -f $args[0]))
                } -ArgumentList $_
            } |
            Receive-Job -Wait -AutoRemoveJob | Out-Null
    }

    # thread-based job - requires the ThreadJob module
    if (-not $noStartThreadJob) {
        # If Start-ThreadJob is available, add an approach for it.
        $approachImpl['Start-ThreadJob'] = {
            param([array] $batch)
            $batch |
                ForEach-Object {
                    Start-ThreadJob -ThrottleLimit $BatchSize {
                        Invoke-Expression ($using:exe + ' ' + ($using:exeArgList -f $args[0]))
                    } -ArgumentList $_
                } |
                Receive-Job -Wait -AutoRemoveJob | Out-Null
        }
    }

    # ForEach-Object -Parallel job
    if (-not $noForEachParallel) {
        $approachImpl['ForEach-Object -Parallel'] = {
            param([array] $batch)
            $batch | ForEach-Object -ThrottleLimit $BatchSize -Parallel {
                Invoke-Expression ($using:exe + ' ' + ($using:exeArgList -f $_))
            }
        }
    }

    # direct execution of an external program
    $approachImpl['Start-Process'] = {
        param([array] $batch)
        $batch |
            ForEach-Object {
                Start-Process -NoNewWindow -PassThru $exe -ArgumentList ($exeArgList -f $_)
            } |
            Wait-Process
    }

    # Partition the array of all indices into subarrays (batches)
    $batches = @(
        0..([math]::Ceiling($zipFiles.Count / $batchSize) - 1) | ForEach-Object {
            , $zipFiles[($_ * $batchSize)..($_ * $batchSize + $batchSize - 1)]
        }
    )

    $tsTotals = foreach ($appr in $approaches) {
        $i = 0
        $tsTotal = [timespan] 0
        $batches | ForEach-Object {
            Write-Verbose "$batchSize-element '$appr' batch"
            $ts = Measure-Command { & $approachImpl[$appr] $_ | Out-Null }
            $tsTotal += $ts
            if (++$i -eq $batches.Count) {
                # last batch processed.
                if ($batches.Count -gt 1) {
                    Write-Verbose ("'$appr' processing $JobCount items finished in " +
                        "$($tsTotal.TotalSeconds.ToString('N2')) secs.")
                }
                $tsTotal # output the overall timing for this approach
            }
        }
    }

    # Output a result object with the overall timings.
    $oht = [ordered] @{}
    $oht['JobCount'] = $JobCount
    $oht['BatchSize'] = $BatchSize
    $oht['BatchCount'] = $batches.Count
    $i = 0
    foreach ($appr in $approaches) {
        $oht[($appr + ' (secs.)')] = $tsTotals[$i++].TotalSeconds.ToString('N2')
    }
    [pscustomobject] $oht
}

次の例では、 Measure-Parallel を使用して 20 個のジョブを並列実行し、一度に 5 個のジョブを実行します。

Measure-Parallel -Approach All -BatchSize 5 -JobCount 20 -Verbose

次の出力は、PowerShell 7.5.1 を実行している Windows コンピューターから出力されます。 タイミングは多くの要因によって異なる場合がありますが、比率によって相対的なパフォーマンスが得られます。

VERBOSE: 5-element 'Start-Job' batch
VERBOSE: 5-element 'Start-Job' batch
VERBOSE: 5-element 'Start-Job' batch
VERBOSE: 5-element 'Start-Job' batch
VERBOSE: 'Start-Job' processing 20 items finished in 7.58 secs.
VERBOSE: 5-element 'Start-ThreadJob' batch
VERBOSE: 5-element 'Start-ThreadJob' batch
VERBOSE: 5-element 'Start-ThreadJob' batch
VERBOSE: 5-element 'Start-ThreadJob' batch
VERBOSE: 'Start-ThreadJob' processing 20 items finished in 2.37 secs.
VERBOSE: 5-element 'Start-Process' batch
VERBOSE: 5-element 'Start-Process' batch
VERBOSE: 5-element 'Start-Process' batch
VERBOSE: 5-element 'Start-Process' batch
VERBOSE: 'Start-Process' processing 20 items finished in 0.26 secs.
VERBOSE: 5-element 'ForEach-Object -Parallel' batch
VERBOSE: 5-element 'ForEach-Object -Parallel' batch
VERBOSE: 5-element 'ForEach-Object -Parallel' batch
VERBOSE: 5-element 'ForEach-Object -Parallel' batch
VERBOSE: 'ForEach-Object -Parallel' processing 20 items finished in 0.79 secs.

JobCount                         : 20
BatchSize                        : 5
BatchCount                       : 4
Start-Job (secs.)                : 7.58
Start-ThreadJob (secs.)          : 2.37
Start-Process (secs.)            : 0.26
ForEach-Object -Parallel (secs.) : 0.79

まとめ

  • Start-Processアプローチは、ジョブ管理のオーバーヘッドがないため、最適に実行されます。 ただし、前述のように、このアプローチには基本的な制限があります。
  • ForEach-Object -Parallelでは、オーバーヘッドが最も少ない後に、Start-ThreadJobが追加されます。
  • Start-Job には、ジョブごとに作成される非表示の PowerShell インスタンスがあるため、オーバーヘッドが最も大きくなります。

謝辞

情報の多くは、この記事は、このスタックオーバーフローの投稿でサンティアゴ・スクエアゾンmklement0 からの回答に基づいています。

サンティアゴ・スクエアゾンによって作成された PSParallelPipeline モジュールにも興味があるかもしれません。

詳細については、次を参照してください。