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

Skrypty programu PowerShell, które bezpośrednio wykorzystują program .NET i unikają potoku, są szybsze niż idiomatyczny program PowerShell. Idiomatyczny program PowerShell zwykle używa poleceń cmdlet i funkcji programu PowerShell w dużym stopniu, często wykorzystując potok i porzucając go do środowiska .NET tylko wtedy, gdy jest to konieczne.

Uwaga

Wiele technik opisanych w tym miejscu nie jest idiomatycznych programu PowerShell i może zmniejszyć czytelność skryptu programu PowerShell. Autorom skryptów zaleca się używanie idiomatycznego programu PowerShell, chyba że wydajność nie dyktuje inaczej.

Pomijanie danych wyjściowych

Istnieje wiele sposobów, aby uniknąć zapisywania obiektów w potoku:

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

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

$arrayList.Add($item) > $null

Przekierowywanie plików do jest niemal tak dobre jak poprzednie alternatywy, większość skryptów nigdy $null nie zauważy różnicy. Jednak w zależności od scenariusza przekierowywanie plików powoduje niewielkie obciążenie.

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

Można również potokować do Out-Null . W programie PowerShell 7.x jest to nieco wolniejsze niż przekierowywanie, ale prawdopodobnie nie jest zauważalne w przypadku większości skryptów. Jednak wywoływanie w dużej pętli może być Out-Null 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 programu PowerShell 7.x, dlatego należy unikać używania w kodzie Out-Null Out-Null wrażliwym na wydajność.

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

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

Ta technika działa mniej więcej tak dobrze, jak przekierowywuje dane do i należy unikać ich Out-Null w skryptach wrażliwych na wydajność. Dodatkowe obciążenie w tym przykładzie wynika z utworzenia i wywołania bloku skryptu, który był wcześniej skryptem w tekście.

Dodawanie tablicy

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

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

Może to być bardzo niewydajne, ponieważ tablice są niezmienne. Każdy dodatek do tablicy faktycznie tworzy nową tablicę wystarczająco dużą, aby pomieścić wszystkie elementy lewego i prawego operandu, a następnie kopiuje elementy obu operandów do nowej tablicy. W przypadku małych kolekcji ten narzut może nie mieć znaczenia. W przypadku dużych kolekcji na pewno może to być problemem.

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

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

Jeśli potrzebujesz tablicy, możesz użyć własnej i po prostu wywołać element ArrayList ArrayList.ToArray , gdy chcesz, aby tablica. Alternatywnie możesz pozwolić programowi PowerShell na utworzenie ArrayList poleceń i Array dla Ciebie:

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

W tym przykładzie program PowerShell tworzy element do przechowywania wyników zapisywanych w ArrayList potoku wewnątrz wyrażenia tablicy. Tuż przed przypisaniem do $results polecenia program PowerShell konwertuje ArrayList element na element object[] .

Dodawanie ciągu

Podobnie jak tablice, ciągi są niezmienne. Każdy dodatek do ciągu faktycznie tworzy nowy ciąg wystarczająco duży, aby pomieścić zawartość lewego i prawego operandu, a następnie kopiuje elementy obu operandów do nowego ciągu. W przypadku małych ciągów ten narzut może nie mieć znaczenia. W przypadku dużych ciągów na pewno może to być problemem.

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

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

Istnieje kilka alternatyw. Do połączenia -join ciągów można użyć operatora .

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

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

W tym przykładzie użycie -join operatora jest prawie 30-krotnie szybsze 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 StringBuilder jest prawie 50 razy szybsze niż dodawanie ciągu.

Przetwarzanie dużych plików

Idiomatyczny sposób przetwarzania pliku w programie PowerShell może wyglądać podobnie do:

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

Może to być prawie o rząd wielkości wolniejsze niż bezpośrednie korzystanie z interfejsów API .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 zapis danych wyjściowych bezpośrednio w konsoli jest uznawany za zły, ale gdy ma to sens, wiele skryptów używa polecenia Write-Host .

Jeśli musisz zapisać wiele komunikatów w konsoli, element może być o rząd wielkości Write-Host wolniejszy niż [Console]::WriteLine() . Należy jednak pamiętać, że jest to odpowiednia alternatywa tylko dla [Console]::WriteLine() określonych hostów, takich jak pwsh.exe , lub powershell.exe powershell_ise.exe . Nie ma gwarancji, że będzie działać na wszystkich hostach. Ponadto dane wyjściowe zapisywane przy [Console]::WriteLine() użyciu nie są zapisywane w transkrypcjach uruchomionych przez . Start-Transcript

Zamiast używać funkcji Write-Host , rozważ użycie write-output.

Kompilacja JIT

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

Pętle, które mają mniej niż 300 instrukcji, kwalifikują się do kompilacji JIT. Pętle większe niż te są zbyt kosztowne do skompilowania. Gdy pętla jest wykonywana 16 razy, skrypt jest kompilowany jit w tle. Po zakończeniu kompilacji JIT wykonywanie jest przenoszone do skompilowanego kodu.

Unikanie powtarzających się wywołań funkcji

Wywołanie funkcji może być kosztowną operacją. Jeśli funkcja jest wywoływana 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 podstawowej pętli for jest linią bazową wydajności. Drugi przykład opakuje generator liczb losowych w funkcję, która jest wywoływana w ścisłej 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