Partager via


Optimiser les performances à l’aide de l’exécution parallèle

PowerShell fournit plusieurs options pour la création d’appels parallèles.

  • Start-Job exécute chaque travail dans un processus distinct, chacun avec une nouvelle instance de PowerShell. Dans de nombreux cas, une boucle linéaire est plus rapide. En outre, la sérialisation et la désérialisation peuvent limiter l’utilité des objets retournés. Cette commande est intégrée à toutes les versions de PowerShell.
  • Start-ThreadJob est une applet de commande trouvée dans le module ThreadJob . Cette commande utilise des runspaces PowerShell pour créer et gérer des travaux basés sur des threads. Ces travaux sont plus légers que les travaux créés par Start-Job et évitent la perte potentielle de fidélité de type requise par la sérialisation et la désérialisation entre processus. Le module ThreadJob est fourni avec PowerShell 7 et versions ultérieures. Pour Windows PowerShell 5.1, vous pouvez installer ce module à partir de PowerShell Gallery.
  • Utilisez l’espace de noms System.Management.Automation.Runspaces à partir du Kit de développement logiciel (SDK) PowerShell pour créer votre propre logique parallèle. Les deux ForEach-Object -Parallel et Start-ThreadJob utilisent des runspaces PowerShell pour exécuter le code en parallèle.
  • Les flux de travail sont une fonctionnalité de Windows PowerShell 5.1. Les flux de travail ne sont pas disponibles dans PowerShell 7.0 et versions ultérieures. Les flux de travail sont un type spécial de script PowerShell qui peut s’exécuter en parallèle. Ils sont conçus pour les tâches de longue durée et peuvent être suspendus et repris. Les flux de travail ne sont pas recommandés pour le nouveau développement. Pour plus d’informations, consultez about_Workflows.
  • ForEach-Object -Parallel est une fonctionnalité de PowerShell 7.0 et versions ultérieures. Comme Start-ThreadJob, il utilise des runspaces PowerShell pour créer et gérer des travaux basés sur des threads. Cette commande est conçue pour une utilisation dans un pipeline.

Limiter la simultanéité d'exécution

L’exécution de scripts en parallèle ne garantit pas d’amélioration des performances. Par exemple, les scénarios suivants peuvent tirer parti de l’exécution parallèle :

  • Calcul de scripts intensifs sur des processeurs multicœurs multithreads
  • Scripts qui passent du temps à attendre des résultats ou à effectuer des opérations de fichier, tant que ces opérations ne se bloquent pas les unes les autres.

Il est important d’équilibrer la surcharge de l’exécution parallèle avec le type de travail effectué. En outre, il existe des limites au nombre d’appels qui peuvent s’exécuter en parallèle.

Les commandes Start-ThreadJob et ForEach-Object -Parallel ont un paramètre ThrottleLimit pour limiter le nombre de travaux en cours d’exécution à la fois. À mesure que d’autres travaux sont démarrés, ils sont mis en file d’attente et attendent que le nombre actuel de travaux tombe en dessous de la limite de limitation. À partir de PowerShell 7.1, ForEach-Object -Parallel réutilise les espaces d’exécution à partir d’un pool d’instances d’exécution par défaut. Le paramètre ThrottleLimit définit la taille du pool d’instances d’exécution. La taille du pool runspace par défaut est 5. Vous pouvez toujours créer un espace d’exécution pour chaque itération à l’aide du commutateur UseNewRunspace.

La Start-Job commande n’a pas de paramètre ThrottleLimit . Vous devez gérer le nombre de travaux en cours d’exécution à la fois.

Mesurer les performances

La fonction suivante, compare Measure-Parallella vitesse des approches d’exécution parallèle suivantes :

  • Start-Job - crée un processus PowerShell enfant derrière les coulisses

  • Start-ThreadJob - exécute chaque travail dans un thread distinct

  • ForEach-Object -Parallel - exécute chaque travail dans un thread distinct

  • Start-Process - appelle un programme externe de façon asynchrone

    Remarque

    Cette approche n’est logique que si vos tâches parallèles se composent uniquement d’un seul appel à un programme externe, par opposition à l’exécution d’un bloc de code PowerShell. En outre, la seule façon de capturer la sortie avec cette approche consiste à rediriger vers un fichier.

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
}

L’exemple suivant utilise Measure-Parallel pour exécuter 20 travaux en parallèle, 5 à la fois, à l’aide de toutes les approches disponibles.

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

La sortie suivante provient d’un ordinateur Windows exécutant PowerShell 7.5.1. Votre timing peut varier en fonction de nombreux facteurs, mais les ratios doivent donner une idée des performances relatives.

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

Conclusions

  • L’approche Start-Process est optimale, car elle n’a pas la surcharge de la gestion des travaux. Toutefois, comme indiqué précédemment, cette approche présente des limitations fondamentales.
  • ForEach-Object -Parallel génère la moindre surcharge, suivi par Start-ThreadJob.
  • Start-Job a la charge la plus élevée en raison des instances PowerShell masquées qu’il crée pour chaque travail.

Remerciements

Une grande partie de l’information de cet article est basée sur les réponses de Santiago Squarzon et mklement0 dans cette publication sur Stack Overflow.

Vous pouvez également être intéressé par le module PSParallelPipeline créé par Santiago Squarzon.

Lectures complémentaires