Prestandaöverväganden för PowerShell-skript

PowerShell-skript som utnyttjar .NET direkt och undviker pipelinen tenderar att vara snabbare än idiomatisk PowerShell. Idiomatisk PowerShell använder vanligtvis cmdletar och PowerShell-funktioner kraftigt, ofta med hjälp av pipelinen, och droppar ned till .NET endast när det behövs.

Anteckning

Många av de tekniker som beskrivs här är inte idiomatiska PowerShell och kan minska läsbarheten för ett PowerShell-skript. Skriptförfattare rekommenderas att använda idiomatisk PowerShell om inte prestandan kräver något annat.

Utelämna utdata

Det finns många sätt att undvika att skriva objekt till pipelinen:

$null = $arrayList.Add($item)
[void]$arrayList.Add($item)

Tilldelningen till $null eller omvandlingen till [void] är ungefär likvärdig och bör vanligtvis föredras när prestanda är viktigt.

$arrayList.Add($item) > $null

Filomdirigering till $null är nästan lika bra som de tidigare alternativen. De flesta skript skulle aldrig märka skillnaden. Beroende på scenariot medför filomdirigering dock lite extraarbete.

$arrayList.Add($item) | Out-Null

Du kan också skicka till Out-Null. I PowerShell 7.x är detta lite långsammare än omdirigering, men förmodligen inte märkbart för de flesta skript. Men att anropa Out-Null i en stor loop kan vara betydligt långsammare, även i PowerShell 7.x.

$d = Get-Date
Measure-Command { for($i=0; $i -lt 1mb; $i++) { $null=$d } } |
    Select-Object TotalSeconds

TotalSeconds
------------
   1.0549325

$d = Get-Date
Measure-Command { for($i=0; $i -lt 1mb; $i++) { $d | Out-Null } } |
    Select-Object TotalSeconds

TotalSeconds
------------
   5.9572186

Windows PowerShell 5.1 har inte samma optimeringar för Out-Null som PowerShell 7.x, så du bör undvika att använda Out-Null i prestandakänslig kod.

Att introducera ett skriptblock och anropa det (med punktkällor eller på annat sätt) och sedan tilldela resultatet till $null är en praktisk teknik för att undertrycka utdata från ett stort skriptblock.

$null = . {
    $arrayList.Add($item)
    $arrayList.Add(42)
}

Den här tekniken fungerar ungefär lika bra som rördragning till Out-Null och bör undvikas i prestandakänsligt skript. Det extra arbetet i det här exemplet kommer från skapandet av och anropet av ett skriptblock som tidigare var infogat skript.

Matristillägg

Att generera en lista över objekt görs ofta med hjälp av en matris med additionsoperatorn:

$results = @()
$results += Do-Something
$results += Do-SomethingElse
$results

Detta kan vara mycket ineffektivt eftersom matriser är oföränderliga. Varje tillägg till matrisen skapar faktiskt en ny matris som är tillräckligt stor för att innehålla alla element i både vänster och höger operander och kopierar sedan elementen i båda operanderna till den nya matrisen. För små samlingar spelar det här arbetet kanske ingen roll. För stora samlingar kan detta definitivt vara ett problem.

Det finns ett par alternativ. Om du inte behöver en matris bör du i stället överväga att använda en ArrayList:

$results = [System.Collections.ArrayList]::new()
$results.AddRange((Do-Something))
$results.AddRange((Do-SomethingElse))
$results

Om du behöver en matris kan du använda din egen ArrayList och helt enkelt anropa ArrayList.ToArray när du vill ha matrisen. Du kan också låta PowerShell skapa ArrayList och Array åt dig:

$results = @(
    Do-Something
    Do-SomethingElse
)

I det här exemplet skapar PowerShell en ArrayList för att lagra resultaten som skrivits till pipelinen i matrisuttrycket. Precis innan du tilldelar till $resultskonverterar ArrayList PowerShell till en object[].

Tillägg av strängar

Precis som matriser är strängar oföränderliga. Varje tillägg till strängen skapar faktiskt en ny sträng som är tillräckligt stor för att innehålla innehållet i både de vänstra och högra operanderna och kopierar sedan elementen i båda operanderna till den nya strängen. För små strängar spelar det här arbetet kanske ingen roll. För stora strängar kan detta definitivt vara ett problem.

$string = ''
Measure-Command {
      foreach( $i in 1..10000)
      {
          $string += "Iteration $i`n"
      }
      $string
  } | Select-Object TotalMilliseconds

TotalMilliseconds
-----------------
         641.8168

Det finns ett par alternativ. Du kan använda operatorn -join för att sammanfoga strängar.

Measure-Command {
      $string = @(
          foreach ($i in 1..10000) { "Iteration $i" }
      ) -join "`n"
      $string
  } | Select-Object TotalMilliseconds

TotalMilliseconds
-----------------
          22.7069

I det här exemplet är det nästan 30 gånger snabbare att använda operatorn -join än att lägga till strängar.

Du kan också använda klassen .NET StringBuilder .

$sb = [System.Text.StringBuilder]::new()
Measure-Command {
      foreach( $i in 1..10000)
      {
          [void]$sb.Append("Iteration $i`n")
      }
      $sb.ToString()
  } | Select-Object TotalMilliseconds

TotalMilliseconds
-----------------
          13.4671

I det här exemplet är det nästan 50 gånger snabbare att använda StringBuilder än att lägga till strängar.

Bearbeta stora filer

Det idiomatiska sättet att bearbeta en fil i PowerShell kan se ut ungefär så här:

Get-Content $path | Where-Object { $_.Length -gt 10 }

Det kan vara nästan en storleksordning som är långsammare än att använda .NET-API:er direkt:

try
{
    $stream = [System.IO.StreamReader]::new($path)
    while ($line = $stream.ReadLine())
    {
        if ($line.Length -gt 10)
        {
            $line
        }
    }
}
finally
{
    $stream.Dispose()
}

Undvik Write-Host

Det anses allmänt vara dålig praxis att skriva utdata direkt till konsolen, men när det är logiskt använder Write-Hostmånga skript .

Om du måste skriva många meddelanden till konsolen Write-Host kan det vara en storleksordning som är långsammare än [Console]::WriteLine(). Tänk dock på att [Console]::WriteLine() endast är ett lämpligt alternativ för specifika värdar som pwsh.exe, powershell.exeeller powershell_ise.exe. Det är inte säkert att det fungerar på alla värdar. Utdata som skrivs med [Console]::WriteLine() skrivs inte heller till avskrifter som startas av Start-Transcript.

Överväg att använda Write-Output i stället för att använda Write-Host.

JIT-kompilering

PowerShell kompilerar skriptkoden till bytekod som tolkas. Från och med PowerShell 3, för kod som körs upprepade gånger i en loop, kan PowerShell förbättra prestanda genom att JIT (Just-in-time) kompilerar koden till intern kod.

Loopar som har färre än 300 instruktioner är berättigade till JIT-kompilering. Loopar som är större än så är för kostsamma för att kompileras. När loopen har körts 16 gånger är skriptet JIT-kompilerat i bakgrunden. När JIT-kompilering har slutförts överförs körningen till den kompilerade koden.

Undvik upprepade anrop till en funktion

Att anropa en funktion kan vara en dyr åtgärd. Om du anropar en funktion i en tidskrävande tight loop bör du överväga att flytta loopen inuti funktionen.

Överväg följande exempel:

$ranGen = New-Object System.Random
$RepeatCount = 10000

'Basic for-loop = {0}ms' -f (Measure-Command -Expression {
    for ($i = 0; $i -lt $RepeatCount; $i++) {
        $Null = $ranGen.Next()
    }
}).TotalMilliseconds

'Wrapped in a function = {0}ms' -f (Measure-Command -Expression {
    function Get-RandNum_Core {
        param ($ranGen)
        $ranGen.Next()
    }

    for ($i = 0; $i -lt $RepeatCount; $i++) {
        $Null = Get-RandNum_Core $ranGen
    }
}).TotalMilliseconds

'For-loop in a function = {0}ms' -f (Measure-Command -Expression {
    function Get-RandNum_All {
        param ($ranGen)
        for ($i = 0; $i -lt $RepeatCount; $i++) {
            $Null = $ranGen.Next()
        }
    }

    Get-RandNum_All $ranGen
}).TotalMilliseconds

Basic for-loop-exemplet är baslinjen för prestanda. Det andra exemplet omsluter slumptalsgeneratorn i en funktion som anropas i en tät loop. Det tredje exemplet flyttar loopen inuti funktionen. Funktionen anropas bara en gång, men koden genererar fortfarande 10 000 slumpmässiga tal. Observera skillnaden i körningstider för varje exempel.

Basic for-loop = 47.8668ms
Wrapped in a function = 820.1396ms
For-loop in a function = 23.3193ms