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 używa poleceń cmdlet i funkcji programu PowerShell, często korzystających z potoku i uciekających się do platformy .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. Autorzy skryptów powinni używać idiotycznego programu PowerShell, chyba że wydajność będzie dyktowała inaczej.
Pomijanie danych wyjściowych
Istnieje wiele sposobów, aby uniknąć zapisywania obiektów w potoku.
- Przypisywanie do
$null
- Rzutowanie do
[void]
- Przekierowanie pliku do
$null
- Potok do
Out-Null
Szybkość przypisywania do metody , rzutowania do $null
[void]
metody i przekierowania pliku do $null
metody jest prawie identyczna. Jednak wywołanie Out-Null
w dużej pętli może być znacznie wolniejsze, zwłaszcza w programie PowerShell 5.1.
$tests = @{
'Assign to $null' = {
$arrayList = [System.Collections.ArrayList]::new()
foreach ($i in 0..$args[0]) {
$null = $arraylist.Add($i)
}
}
'Cast to [void]' = {
$arrayList = [System.Collections.ArrayList]::new()
foreach ($i in 0..$args[0]) {
[void]$arraylist.Add($i)
}
}
'Redirect to $null' = {
$arrayList = [System.Collections.ArrayList]::new()
foreach ($i in 0..$args[0]) {
$arraylist.Add($i) > $null
}
}
'Pipe to Out-Null' = {
$arrayList = [System.Collections.ArrayList]::new()
foreach ($i in 0..$args[0]) {
$arraylist.Add($i) | Out-Null
}
}
}
10kb, 50kb, 100kb | ForEach-Object {
$groupResult = foreach ($test in $tests.GetEnumerator()) {
$ms = (Measure-Command { & $test.Value $_ }).TotalMilliseconds
[pscustomobject]@{
Iterations = $_
Test = $test.Key
TotalMilliseconds = [math]::Round($ms, 2)
}
[GC]::Collect()
[GC]::WaitForPendingFinalizers()
}
$groupResult = $groupResult | Sort-Object TotalMilliseconds
$groupResult | Select-Object *, @{
Name = 'RelativeSpeed'
Expression = {
$relativeSpeed = $_.TotalMilliseconds / $groupResult[0].TotalMilliseconds
[math]::Round($relativeSpeed, 2).ToString() + 'x'
}
}
}
Te testy zostały uruchomione na maszynie z systemem Windows 11 w programie PowerShell 7.3.4. Poniżej przedstawiono wyniki:
Iterations Test TotalMilliseconds RelativeSpeed
---------- ---- ----------------- -------------
10240 Assign to $null 36.74 1x
10240 Redirect to $null 55.84 1.52x
10240 Cast to [void] 62.96 1.71x
10240 Pipe to Out-Null 81.65 2.22x
51200 Assign to $null 193.92 1x
51200 Cast to [void] 200.77 1.04x
51200 Redirect to $null 219.69 1.13x
51200 Pipe to Out-Null 329.62 1.7x
102400 Redirect to $null 386.08 1x
102400 Assign to $null 392.13 1.02x
102400 Cast to [void] 405.24 1.05x
102400 Pipe to Out-Null 572.94 1.48x
Czasy i względne szybkości mogą się różnić w zależności od sprzętu, wersji programu PowerShell i bieżącego obciążenia w systemie.
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
Dodanie tablicy jest nieefektywne, ponieważ tablice mają stały rozmiar. Każdy dodatek do tablicy tworzy nową tablicę wystarczająco dużą, aby przechowywać wszystkie elementy zarówno lewego, jak i prawego operandu. Elementy obu operandów są kopiowane do nowej tablicy. W przypadku małych kolekcji to obciążenie może nie mieć znaczenia. Wydajność może być spora w przypadku dużych kolekcji.
Istnieje kilka alternatyw. Jeśli tablica nie jest w rzeczywistości wymagana, rozważ użycie wpisanej listy ogólnej (Lista<T>):
$results = [System.Collections.Generic.List[object]]::new()
$results.AddRange((Do-Something))
$results.AddRange((Do-SomethingElse))
$results
Wpływ na wydajność użycia dodawania tablicy rośnie wykładniczo wraz z rozmiarem kolekcji i liczbą dodatków. Ten kod porównuje jawne przypisywanie wartości do tablicy przy użyciu dodawania tablicy i używanie Add()
metody na liście<T>. Definiuje jawne przypisanie jako punkt odniesienia dla wydajności.
$tests = @{
'PowerShell Explicit Assignment' = {
param($count)
$result = foreach($i in 1..$count) {
$i
}
}
'.Add(..) to List<T>' = {
param($count)
$result = [Collections.Generic.List[int]]::new()
foreach($i in 1..$count) {
$result.Add($i)
}
}
'+= Operator to Array' = {
param($count)
$result = @()
foreach($i in 1..$count) {
$result += $i
}
}
}
5kb, 10kb, 100kb | ForEach-Object {
$groupResult = foreach($test in $tests.GetEnumerator()) {
$ms = (Measure-Command { & $test.Value -Count $_ }).TotalMilliseconds
[pscustomobject]@{
CollectionSize = $_
Test = $test.Key
TotalMilliseconds = [math]::Round($ms, 2)
}
[GC]::Collect()
[GC]::WaitForPendingFinalizers()
}
$groupResult = $groupResult | Sort-Object TotalMilliseconds
$groupResult | Select-Object *, @{
Name = 'RelativeSpeed'
Expression = {
$relativeSpeed = $_.TotalMilliseconds / $groupResult[0].TotalMilliseconds
[math]::Round($relativeSpeed, 2).ToString() + 'x'
}
}
}
Te testy zostały uruchomione na maszynie z systemem Windows 11 w programie PowerShell 7.3.4.
CollectionSize Test TotalMilliseconds RelativeSpeed
-------------- ---- ----------------- -------------
5120 PowerShell Explicit Assignment 26.65 1x
5120 .Add(..) to List<T> 110.98 4.16x
5120 += Operator to Array 402.91 15.12x
10240 PowerShell Explicit Assignment 0.49 1x
10240 .Add(..) to List<T> 137.67 280.96x
10240 += Operator to Array 1678.13 3424.76x
102400 PowerShell Explicit Assignment 11.18 1x
102400 .Add(..) to List<T> 1384.03 123.8x
102400 += Operator to Array 201991.06 18067.18x
Podczas pracy z dużymi kolekcjami dodawanie tablicy jest znacznie wolniejsze niż dodawanie do listy<T>.
W przypadku korzystania z listy<T> należy utworzyć listę z określonym typem, na przykład Ciąg lub Int. Po dodaniu obiektów innego typu do listy są one rzutowane do określonego typu. Jeśli nie można rzutować ich do określonego typu, metoda zgłasza wyjątek.
$intList = [System.Collections.Generic.List[int]]::new()
$intList.Add(1)
$intList.Add('2')
$intList.Add(3.0)
$intList.Add('Four')
$intList
MethodException:
Line |
5 | $intList.Add('Four')
| ~~~~~~~~~~~~~~~~~~~~
| Cannot convert argument "item", with value: "Four", for "Add" to type
"System.Int32": "Cannot convert value "Four" to type "System.Int32".
Error: "The input string 'Four' was not in a correct format.""
1
2
3
Jeśli lista musi być kolekcją różnych typów obiektów, utwórz ją z obiektem jako typem listy. Możesz wyliczyć kolekcję, aby sprawdzić typy obiektów w niej.
$objectList = [System.Collections.Generic.List[object]]::new()
$objectList.Add(1)
$objectList.Add('2')
$objectList.Add(3.0)
$objectList.GetEnumerator().ForEach({ "$_ is $($_.GetType())" })
1 is int
2 is string
3 is double
Jeśli potrzebujesz tablicy, możesz wywołać ToArray()
metodę na liście lub umożliwić programowi PowerShell utworzenie tablicy dla Ciebie:
$results = @(
Do-Something
Do-SomethingElse
)
W tym przykładzie program PowerShell tworzy tablicęList do przechowywania wyników zapisanych w potoku wewnątrz wyrażenia tablicy. Tuż przed przypisaniem do $results
programu program PowerShell konwertuje tablicęlist na obiekt[].
Dodawanie ciągu
Ciągi są niezmienne. Każdy dodatek do ciągu faktycznie 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 może to mieć wpływ na wydajność i zużycie pamięci.
Istnieją co najmniej dwie alternatywy:
-join
Operator łączy ciągi- Klasa .NET StringBuilder udostępnia modyfikowalny ciąg
Poniższy przykład porównuje wydajność tych trzech metod tworzenia ciągu.
$tests = @{
'StringBuilder' = {
$sb = [System.Text.StringBuilder]::new()
foreach ($i in 0..$args[0]) {
$sb = $sb.AppendLine("Iteration $i")
}
$sb.ToString()
}
'Join operator' = {
$string = @(
foreach ($i in 0..$args[0]) {
"Iteration $i"
}
) -join "`n"
$string
}
'Addition Assignment +=' = {
$string = ''
foreach ($i in 0..$args[0]) {
$string += "Iteration $i`n"
}
$string
}
}
10kb, 50kb, 100kb | ForEach-Object {
$groupResult = foreach ($test in $tests.GetEnumerator()) {
$ms = (Measure-Command { & $test.Value $_ }).TotalMilliseconds
[pscustomobject]@{
Iterations = $_
Test = $test.Key
TotalMilliseconds = [math]::Round($ms, 2)
}
[GC]::Collect()
[GC]::WaitForPendingFinalizers()
}
$groupResult = $groupResult | Sort-Object TotalMilliseconds
$groupResult | Select-Object *, @{
Name = 'RelativeSpeed'
Expression = {
$relativeSpeed = $_.TotalMilliseconds / $groupResult[0].TotalMilliseconds
[math]::Round($relativeSpeed, 2).ToString() + 'x'
}
}
}
Te testy zostały uruchomione na maszynie z systemem Windows 10 w programie PowerShell 7.3.4. Dane wyjściowe pokazują, że -join
operator jest najszybszy, a następnie klasą StringBuilder .
Iterations Test TotalMilliseconds RelativeSpeed
---------- ---- ----------------- -------------
10240 Join operator 7.08 1x
10240 StringBuilder 54.10 7.64x
10240 Addition Assignment += 724.16 102.28x
51200 Join operator 41.76 1x
51200 StringBuilder 318.06 7.62x
51200 Addition Assignment += 17693.06 423.68x
102400 Join operator 106.98 1x
102400 StringBuilder 543.84 5.08x
102400 Addition Assignment += 90693.13 847.76x
Czasy i względne szybkości mogą się różnić w zależności od sprzętu, wersji programu PowerShell i bieżącego obciążenia w systemie.
Przetwarzanie dużych plików
Idiomatyczny sposób przetwarzania pliku w programie PowerShell może wyglądać następująco:
Get-Content $path | Where-Object { $_.Length -gt 10 }
Może to być kolejność wielkości wolniej 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()
}
Szukanie wpisów według właściwości w dużych kolekcjach
Często należy używać właściwości udostępnionej do identyfikowania tego samego rekordu w różnych kolekcjach, na przykład przy użyciu nazwy w celu pobrania identyfikatora z jednej listy i wiadomości e-mail z innej. Iterowanie na pierwszej liście w celu znalezienia pasującego rekordu w drugiej kolekcji jest powolne. W szczególności powtarzające się filtrowanie drugiej kolekcji ma duże obciążenie.
Biorąc pod uwagę dwie kolekcje, jedną z identyfikatorem i nazwą, drugą z nazwą i adresem e-mail:
$Employees = 1..10000 | ForEach-Object {
[PSCustomObject]@{
Id = $_
Name = "Name$_"
}
}
$Accounts = 2500..7500 | ForEach-Object {
[PSCustomObject]@{
Name = "Name$_"
Email = "Name$_@fabrikam.com"
}
}
Zwykle sposób uzgadniania tych kolekcji w celu zwrócenia listy obiektów z właściwościami IDENTYFIKATOR, Nazwa i Adres e-mail może wyglądać następująco:
$Results = $Employees | ForEach-Object -Process {
$Employee = $_
$Account = $Accounts | Where-Object -FilterScript {
$_.Name -eq $Employee.Name
}
[pscustomobject]@{
Id = $Employee.Id
Name = $Employee.Name
Email = $Account.Email
}
}
Jednak ta implementacja musi filtrować wszystkie 5000 elementów w $Accounts
kolekcji raz dla każdego elementu w $Employee
kolekcji. Może to potrwać kilka minut, nawet w przypadku tego wyszukiwania pojedynczej wartości.
Zamiast tego możesz utworzyć tabelę skrótów, która używa właściwości nazwa współużytkowanej jako klucza i pasującego konta jako wartości.
$LookupHash = @{}
foreach ($Account in $Accounts) {
$LookupHash[$Account.Name] = $Account
}
Szukanie kluczy w tabeli skrótów jest znacznie szybsze niż filtrowanie kolekcji według wartości właściwości. Zamiast sprawdzać każdy element w kolekcji, program PowerShell może sprawdzić, czy klucz jest zdefiniowany i używać jego wartości.
$Results = $Employees | ForEach-Object -Process {
$Email = $LookupHash[$_.Name].Email
[pscustomobject]@{
Id = $_.Id
Name = $_.Name
Email = $Email
}
}
Jest to znacznie szybsze. Podczas gdy filtr pętli trwał kilka minut, wyszukiwanie skrótu trwa mniej niż sekundę.
Unikaj zapisu hosta
Zazwyczaj uważa się, że słabe rozwiązanie w przypadku zapisywania danych wyjściowych bezpośrednio w konsoli, ale jeśli ma to sens, wiele skryptów używa metody Write-Host
.
Jeśli musisz napisać wiele komunikatów w konsoli programu , Write-Host
może być o wiele większe niż [Console]::WriteLine()
w przypadku określonych hostów, takich jak pwsh.exe
, powershell.exe
lub powershell_ise.exe
. Jednak nie ma gwarancji, [Console]::WriteLine()
że działa we wszystkich hostach. Ponadto dane wyjściowe zapisywane przy użyciu [Console]::WriteLine()
polecenia nie są zapisywane w transkrypcjach rozpoczętych przez program Start-Transcript
.
Zamiast używać Write-Host
metody , rozważ użycie funkcji Write-Output.
Kompilacja trybu 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 wielokrotnie wykonywany w pętli, program PowerShell może zwiększyć wydajność dzięki kompilowaniu 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 została wykonana 16 razy, skrypt jest kompilowany w tle. Po zakończeniu kompilacji JIT wykonywanie 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 ciasnej pętli, rozważ przeniesienie pętli wewnątrz funkcji.
Rozważ 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 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ę czasu wykonywania dla każdego przykładu.
Basic for-loop = 47.8668ms
Wrapped in a function = 820.1396ms
For-loop in a function = 23.3193ms
Unikaj zawijania potoków poleceń cmdlet
Większość poleceń cmdlet jest implementowanych dla potoku, co jest sekwencyjną składnią i procesem. Na przykład:
cmdlet1 | cmdlet2 | cmdlet3
Inicjowanie nowego potoku może być kosztowne, dlatego należy unikać zawijania potoku poleceń cmdlet do innego istniejącego potoku.
Rozważmy następujący przykład. Plik Input.csv
zawiera 2100 wierszy. Polecenie Export-Csv
jest opakowane wewnątrz potoku ForEach-Object
. Polecenie Export-Csv
cmdlet jest wywoływane dla każdej iteracji ForEach-Object
pętli.
'Wrapped = {0:N2} ms' -f (Measure-Command -Expression {
Import-Csv .\Input.csv | ForEach-Object -Begin { $Id = 1 } -Process {
[PSCustomObject]@{
Id = $Id
Name = $_.opened_by
} | Export-Csv .\Output1.csv -Append
}
}).TotalMilliseconds
Wrapped = 15,968.78 ms
W następnym przykładzie Export-Csv
polecenie zostało przeniesione poza ForEach-Object
potok.
W tym przypadku Export-Csv
wywoływana jest tylko raz, ale nadal przetwarza wszystkie obiekty z pustą wartością ForEach-Object
.
'Unwrapped = {0:N2} ms' -f (Measure-Command -Expression {
Import-Csv .\Input.csv | ForEach-Object -Begin { $Id = 2 } -Process {
[PSCustomObject]@{
Id = $Id
Name = $_.opened_by
}
} | Export-Csv .\Output2.csv
}).TotalMilliseconds
Unwrapped = 42.92 ms
Przykład rozpasany jest 372 razy szybszy. Należy również zauważyć, że pierwsza implementacja wymaga parametru Append , który nie jest wymagany do późniejszej implementacji.
Opinia
https://aka.ms/ContentUserFeedback.
Dostępne już wkrótce: W 2024 r. będziemy stopniowo wycofywać zgłoszenia z serwisu GitHub jako mechanizm przesyłania opinii na temat zawartości i zastępować go nowym systemem opinii. Aby uzyskać więcej informacji, sprawdź:Prześlij i wyświetl opinię dla