Aspekty výkonu skriptování PowerShellu

Skripty PowerShellu, které využívají rozhraní .NET přímo a vyhnout se kanálu, bývají rychlejší než idiomaticky PowerShell. Idiomatic PowerShell obvykle používá rutiny a funkce PowerShellu, často využívají kanál a v případě potřeby se spadnou do .NET.

Poznámka

Mnoho technik popsaných zde není idiomatickou powershellovou sadou a může snížit čitelnost skriptu PowerShellu. Autoři skriptů doporučují používat idiomatickou PowerShell, pokud výkon diktuje jinak.

Potlačení výstupu

Existuje mnoho způsobů, jak se vyhnout zápisu objektů do kanálu:

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

Přiřazení nebo $null přetypování [void] je přibližně ekvivalentní a mělo by být obecně upřednostňované v případě, že výkon záleží.

$arrayList.Add($item) > $null

Přesměrování souborů na $null je téměř tak dobré jako u předchozích alternativ, většina skriptů si nikdy nevšimla rozdílu. V závislosti na scénáři ale přesměrování souborů přináší trochu režijní režie.

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

Můžete také rourout na Out-Null. V PowerShellu 7.x je to trochu pomalejší než přesměrování, ale pravděpodobně není patrné pro většinu skriptů. Volání Out-Null ve velké smyčce ale může být výrazně pomalejší, a to i v PowerShellu 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 nemá stejné optimalizace Out-Null jako PowerShell 7.x, takže byste se měli vyhnout použití Out-Null v kódu citlivém na výkon.

Představujeme blok skriptu a zavoláte ho (pomocí dot sourcingu nebo jinak) a následné přiřazení výsledku $null je vhodná technika pro potlačení výstupu velkého bloku skriptu.

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

Tato technika provádí zhruba stejně jako potrubí Out-Null a měla by se vyhnout v citlivém skriptu na výkon. Další režie v tomto příkladu pochází z vytvoření a vyvolání bloku skriptu, který byl dříve vložený skript.

Přidání pole

Generování seznamu položek se často provádí pomocí pole s operátorem sčítání:

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

To může být velmi neefektivní, protože pole jsou neměnná. Každý doplněk pole ve skutečnosti vytvoří novou matici dostatečně velký, aby držel všechny prvky levých i pravých operandů a potom zkopíruje prvky obou operandů do nového pole. U malých kolekcí nemusí být tato režie důležitá. U velkých kolekcí to může být určitě problém.

Existuje několik alternativ. Pokud pole skutečně nepotřebujete, zvažte místo toho použití pole ArrayList:

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

Pokud potřebujete pole, můžete použít vlastní ArrayList a jednoduše volat ArrayList.ToArray , když chcete pole. Případně můžete powershellu povolit vytvoření ArrayList a Array za vás:

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

V tomto příkladu PowerShell vytvoří výsledek ArrayList napsaný do kanálu uvnitř maticového výrazu. Těsně před přiřazením k $resultspowershellu se převede na ArrayList .object[]

Přidání řetězců

Podobně jako pole jsou řetězce neměnné. Každý doplněk k řetězci ve skutečnosti vytvoří nový řetězec dostatečně velký, aby držel obsah levých i pravých operandů a pak zkopíruje prvky obou operandů do nového řetězce. U malýchřetězcůch U velkých řetězců to může být určitě problém.

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

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

Existuje několik alternativ. Operátor -join můžete použít ke zřetězení řetězců.

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

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

V tomto příkladu je použití operátoru -join téměř 30krát rychlejší než sčítání řetězců.

Můžete také použít třídu .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

V tomto příkladu je použití StringBuilderu téměř 50krát rychlejší než přidání řetězců.

Zpracování velkých souborů

Idiomaticický způsob zpracování souboru v PowerShellu může vypadat nějak takto:

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

To může být téměř o řád pomalejší než použití rozhraní .NET API přímo:

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

Vyhněte se Write-Host

Obecně se považuje za špatnou praxi psát výstup přímo do konzoly, ale když dává smysl, mnoho skriptů používá Write-Host.

Pokud musíte do konzoly napsat mnoho zpráv, Write-Host může to být řádově pomalejší než [Console]::WriteLine(). Mějte však na paměti, že [Console]::WriteLine() je pouze vhodná alternativa pro konkrétní hostitele, jako je pwsh.exe, powershell.exenebo powershell_ise.exe. Není zaručeno, že bude fungovat ve všech hostitelích. Výstup napsaný pomocí [Console]::WriteLine() se také nezapisuje do přepisů, které začínají Start-Transcript.

Místo použití zvažte použití Write-Hostvýstupu zápisu.

Kompilace JIT

PowerShell zkompiluje kód skriptu na bajtové kódování, které je interpretováno. Počínaje PowerShellem 3 může PowerShell pro kód, který se spouští opakovaně ve smyčce, zlepšit výkon tím, že kód za běhu (JIT) kompiluje do nativního kódu.

Smyčky, které mají méně než 300 pokynů, mají nárok na kompilaci JIT. Smyčky větší, než jsou příliš nákladné ke kompilaci. Po provedení smyčky 16krát se skript zkompiluje na pozadí. Po dokončení kompilace JIT se provádění přenese do zkompilovaného kódu.

Vyhněte se opakovaným voláním funkce.

Volání funkce může být nákladná operace. Pokud voláte funkci v dlouhotrvající těsné smyčce, zvažte přesunutí smyčky uvnitř funkce.

Představte si následující příklady:

$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

Příklad základní smyčky je základní čára pro výkon. Druhý příklad zabalí generátor náhodných čísel do funkce, která se volá v těsné smyčce. Třetí příklad přesune smyčku uvnitř funkce. Funkce se volá pouze jednou, ale kód stále generuje 10000 náhodných čísel. Všimněte si rozdílu v časech provádění pro každý příklad.

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