Zagadnienia dotyczące wydajności skryptów programu PowerShell

Skrypty programu PowerShell, które korzystają bezpośrednio z platformy .NET i unikają potoku, wydają się być szybsze niż idiomatyczny program PowerShell. Idiomatyczny program PowerShell zwykle używa poleceń cmdlet i funkcji programu PowerShell w dużym stopniu, często korzystających z potoku i upuszczania do platformy .NET tylko wtedy, gdy jest to konieczne.

Uwaga

Wiele technik opisanych w tym miejscu nie jest idiomatycznym programem PowerShell i może zmniejszyć czytelność skryptu programu PowerShell. Autorzy skryptów powinni używać idiotycznego programu PowerShell, chyba że wydajność jest określana inaczej.

Pomijanie danych wyjściowych

Istnieje wiele sposobów unikania zapisywania obiektów w potoku:

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

Przypisanie do $null lub rzutowanie do [void] są w przybliżeniu równoważne i zazwyczaj powinny być preferowane w przypadku, gdy wydajność ma znaczenie.

$arrayList.Add($item) > $null

Przekierowanie pliku do $null jest prawie tak dobre, jak poprzednie alternatywy, większość skryptów nigdy nie zauważy różnicy. W zależności od scenariusza przekierowywanie plików wprowadza jednak trochę nakładu pracy.

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

Możesz również przekazać potok do Out-Null. W programie PowerShell 7.x jest to nieco wolniejsze niż przekierowanie, ale prawdopodobnie nie jest zauważalne dla większości skryptów. Jednak wywoływanie Out-Null w dużej pętli może być znacznie wolniejsze, nawet w programie 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 nie ma tych samych optymalizacji dla Out-Null programu PowerShell 7.x, dlatego należy unikać korzystania z kodu Out-Null wrażliwego na wydajność.

Wprowadzenie bloku skryptu i wywołanie go (przy użyciu określania kropki lub w inny sposób), a następnie przypisanie wyniku do $null jest wygodną techniką pomijania danych wyjściowych dużego bloku skryptu.

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

Ta technika działa w przybliżeniu, jak również potok do Out-Null i należy unikać w skrypcie poufnym dla wydajności. Dodatkowe obciążenie w tym przykładzie pochodzi od utworzenia i wywołania bloku skryptu, który był wcześniej wbudowany.

Dodawanie tablicy

Generowanie listy elementów jest często wykonywane przy użyciu tablicy z operatorem dodawania:

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

Może to być bardzo nieefektywne, ponieważ tablice są niezmienne. Każdy dodatek do tablicy rzeczywiście tworzy nową tablicę wystarczająco dużą, aby pomieścić wszystkie elementy zarówno lewej, jak i prawej operandów, a następnie kopiuje elementy obu operandów do nowej tablicy. W przypadku małych kolekcji to obciążenie może nie mieć znaczenia. W przypadku dużych kolekcji na pewno może to być problem.

Istnieje kilka alternatyw. Jeśli w rzeczywistości nie potrzebujesz tablicy, rozważ użycie tablicy:

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

Jeśli potrzebujesz tablicy, możesz użyć własnej ArrayList i po prostu wywołać ArrayList.ToArray , gdy chcesz tablicy. Alternatywnie możesz zezwolić programowi PowerShell na utworzenie elementu ArrayList i Array dla ciebie:

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

W tym przykładzie program PowerShell tworzy obiekt do ArrayList przechowywania wyników zapisanych w potoku w wyrażeniu tablicy. Tuż przed przypisaniem do $resultsprogramu PowerShell konwertuje element ArrayList na .object[]

Dodawanie ciągu

Podobnie jak tablice, ciągi są niezmienne. Każdy dodatek do ciągu rzeczywiście tworzy nowy ciąg wystarczająco duży, aby przechowywać zawartość zarówno lewych, jak i prawych operandów, a następnie kopiuje elementy obu operandów do nowego ciągu. W przypadku małych ciągów to obciążenie może nie mieć znaczenia. W przypadku dużych ciągów na pewno może to być problem.

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

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

Istnieje kilka alternatyw. Możesz użyć -join operatora do łączenia ciągów.

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

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

W tym przykładzie -join użycie operatora jest prawie 30-krotne szybciej niż dodawanie ciągu.

Można również użyć klasy .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

W tym przykładzie użycie klasy StringBuilder jest prawie 50-krotne szybciej niż dodawanie ciągu.

Przetwarzanie dużych plików

Idiotyczny sposób przetwarzania pliku w programie PowerShell może wyglądać mniej więcej tak:

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

Może to być prawie kolejność wielkości wolniejsza niż bezpośrednie używanie interfejsów API platformy .NET:

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

Unikaj Write-Host

Ogólnie uważa się, że słabe rozwiązanie polega na zapisie danych wyjściowych bezpośrednio do konsoli, ale gdy ma to sens, wiele skryptów używa metody Write-Host.

Jeśli musisz napisać wiele komunikatów do konsoli, Write-Host może być rzędu wielkości wolniej niż [Console]::WriteLine(). Należy jednak pamiętać, że [Console]::WriteLine() jest to tylko odpowiednia alternatywa dla określonych hostów, takich jak pwsh.exe, lub powershell.exepowershell_ise.exe. Nie ma gwarancji, że działa we wszystkich hostach. Ponadto dane wyjściowe napisane przy użyciu [Console]::WriteLine() nie są zapisywane w transkrypcjach rozpoczętych przez .Start-Transcript

Zamiast używać metody Write-Host, rozważ użycie funkcji Write-Output.

Kompilacja JIT

Program PowerShell kompiluje kod skryptu do kodu bajtowego, który jest interpretowany. Począwszy od programu PowerShell 3, w przypadku kodu, który jest wykonywany wielokrotnie w pętli, program PowerShell może zwiększyć wydajność przez kompilowanie kodu just in time (JIT) do kodu natywnego.

Pętle z mniej niż 300 instrukcjami kwalifikują się do kompilacji JIT. Pętle większe niż są zbyt kosztowne do skompilowania. Gdy pętla została wykonana 16 razy, skrypt jest kompilowany w tle. Po zakończeniu kompilacji JIT wykonanie jest przenoszone do skompilowanego kodu.

Unikaj powtarzających się wywołań funkcji

Wywoływanie funkcji może być kosztowną operacją. Jeśli wywołujesz funkcję w długotrwałej ścisłej pętli, rozważ przeniesienie pętli wewnątrz funkcji.

Rozważmy następujące przykłady:

$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

Przykład podstawowy dla pętli to linia podstawowa pod kątem wydajności. Drugi przykład opakowuje generator liczb losowych w funkcji, która jest wywoływana w ciasnej pętli. Trzeci przykład przenosi pętlę wewnątrz funkcji. Funkcja jest wywoływana tylko raz, ale kod nadal generuje 10000 liczb losowych. Zwróć uwagę na różnicę w czasie wykonywania dla każdego przykładu.

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