Prestandaöverväganden för PowerShell-skript
PowerShell-skript som utnyttjar .NET direkt och undviker pipelinen tenderar att vara snabbare än idiomatiska PowerShell. Idiomatic PowerShell använder cmdletar och PowerShell-funktioner, använder ofta pipelinen och använder endast .NET när det behövs.
Kommentar
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.
- Tilldelning eller filomdirigering till
$null
- Gjutning till
[void]
- Rör till
Out-Null
Hastigheterna för tilldelning till $null
, gjutning till [void]
och filomdirigering till $null
är nästan identiska. Det kan dock vara betydligt långsammare att anropa Out-Null
i en stor loop, särskilt i 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'
}
}
}
Dessa tester kördes på en Windows 11-dator i PowerShell 7.3.4. Resultaten visas nedan:
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
Tiderna och de relativa hastigheterna kan variera beroende på maskinvara, version av PowerShell och den aktuella arbetsbelastningen i systemet.
Matristillägg
Det går ofta att generera en lista över objekt med hjälp av en matris med additionsoperatorn:
$results = @()
$results += Get-Something
$results += Get-SomethingElse
$results
Matristillägget är ineffektivt eftersom matriser har en fast storlek. Varje tillägg till matrisen skapar en ny matris som är tillräckligt stor för att innehålla alla element i både vänster och höger operander. Elementen i båda operanderna kopieras till den nya matrisen. För små samlingar kanske den här kostnaden inte spelar någon roll. Prestanda kan bli lidande för stora samlingar.
Det finns ett par alternativ. Om du faktiskt inte behöver en matris bör du i stället överväga att använda en typad allmän lista ([List<T>]
):
$results = [System.Collections.Generic.List[object]]::new()
$results.AddRange((Get-Something))
$results.AddRange((Get-SomethingElse))
$results
Prestandapåverkan av att använda matristillägg växer exponentiellt med samlingens storlek och taltilläggen. Den här koden jämför explicit tilldelning av värden till en matris med att använda matristillägg och använda Add(T)
metoden för ett [List<T>]
objekt. Den definierar explicit tilldelning som baslinje för prestanda.
$tests = @{
'PowerShell Explicit Assignment' = {
param($count)
$result = foreach($i in 1..$count) {
$i
}
}
'.Add(T) 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'
}
}
}
Dessa tester kördes på en Windows 11-dator i PowerShell 7.3.4.
CollectionSize Test TotalMilliseconds RelativeSpeed
-------------- ---- ----------------- -------------
5120 PowerShell Explicit Assignment 26.65 1x
5120 .Add(T) to List<T> 110.98 4.16x
5120 += Operator to Array 402.91 15.12x
10240 PowerShell Explicit Assignment 0.49 1x
10240 .Add(T) to List<T> 137.67 280.96x
10240 += Operator to Array 1678.13 3424.76x
102400 PowerShell Explicit Assignment 11.18 1x
102400 .Add(T) to List<T> 1384.03 123.8x
102400 += Operator to Array 201991.06 18067.18x
När du arbetar med stora samlingar är matristillägget betydligt långsammare än att lägga till i en List<T>
.
När du använder ett [List<T>]
objekt måste du skapa listan med en viss typ, till exempel [String]
eller [Int]
. När du lägger till objekt av en annan typ i listan omvandlas de till den angivna typen. Om de inte kan omvandlas till den angivna typen genererar metoden ett undantag.
$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
När du behöver listan som en samling med olika typer av objekt skapar du den med [Object]
som listtyp. Du kan räkna upp samlingen genom att granska typerna av objekten i den.
$objectList = [System.Collections.Generic.List[object]]::new()
$objectList.Add(1)
$objectList.Add('2')
$objectList.Add(3.0)
$objectList | ForEach-Object { "$_ is $($_.GetType())" }
1 is int
2 is string
3 is double
Om du behöver en matris kan du anropa ToArray()
metoden i listan eller låta PowerShell skapa matrisen åt dig:
$results = @(
Get-Something
Get-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 $results
konverterar [ArrayList]
PowerShell till en [Object[]]
.
Strängtillägg
Strängar är 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 vänster och höger operander och kopierar sedan elementen i båda operanderna till den nya strängen. För små strängar kanske den här kostnaden inte spelar någon roll. För stora strängar kan detta påverka prestanda och minnesförbrukning.
Det finns minst två alternativ:
- Operatorn
-join
sammanfogar strängar - .NET-klassen
[StringBuilder]
innehåller en föränderlig sträng
I följande exempel jämförs prestandan för dessa tre metoder för att skapa en sträng.
$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'
}
}
}
Dessa tester kördes på en Windows 11-dator i PowerShell 7.4.2. Utdata visar att operatorn -join
är snabbast följt av [StringBuilder]
klassen.
Iterations Test TotalMilliseconds RelativeSpeed
---------- ---- ----------------- -------------
10240 Join operator 14.75 1x
10240 StringBuilder 62.44 4.23x
10240 Addition Assignment += 619.64 42.01x
51200 Join operator 43.15 1x
51200 StringBuilder 304.32 7.05x
51200 Addition Assignment += 14225.13 329.67x
102400 Join operator 85.62 1x
102400 StringBuilder 499.12 5.83x
102400 Addition Assignment += 67640.79 790.01x
Tiderna och de relativa hastigheterna kan variera beroende på maskinvara, version av PowerShell och den aktuella arbetsbelastningen i systemet.
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 en storleksordning som är långsammare än att använda .NET-API:er direkt. Du kan till exempel använda .NET-klassen [StreamReader]
:
try {
$reader = [System.IO.StreamReader]::new($path)
while (-not $reader.EndOfStream) {
$line = $reader.ReadLine()
if ($line.Length -gt 10) {
$line
}
}
}
finally {
if ($reader) {
$reader.Dispose()
}
}
Du kan också använda ReadLines
metoden [System.IO.File]
för , som omsluter StreamReader
, förenklar läsprocessen:
foreach ($line in [System.IO.File]::ReadLines($path)) {
if ($line.Length -gt 10) {
$line
}
}
Söka efter poster efter egenskap i stora samlingar
Det är vanligt att behöva använda en delad egenskap för att identifiera samma post i olika samlingar, som att använda ett namn för att hämta ett ID från en lista och ett e-postmeddelande från en annan. Iterering över den första listan för att hitta matchande post i den andra samlingen är långsam. I synnerhet har den upprepade filtreringen av den andra samlingen stora omkostnader.
Två samlingar, en med ID och Namn, den andra med Namn och E-post:
$Employees = 1..10000 | ForEach-Object {
[PSCustomObject]@{
Id = $_
Name = "Name$_"
}
}
$Accounts = 2500..7500 | ForEach-Object {
[PSCustomObject]@{
Name = "Name$_"
Email = "Name$_@fabrikam.com"
}
}
Det vanliga sättet att stämma av dessa samlingar för att returnera en lista över objekt med egenskaperna ID, Namn och E-post kan se ut så här:
$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
}
}
Implementeringen måste dock filtrera alla 5 000 objekt i $Accounts
samlingen en gång för varje objekt i $Employee
samlingen. Det kan ta minuter, även för den här sökningen med ett värde.
I stället kan du skapa en Hash-tabell som använder egenskapen delat namn som en nyckel och det matchande kontot som värde.
$LookupHash = @{}
foreach ($Account in $Accounts) {
$LookupHash[$Account.Name] = $Account
}
Det går mycket snabbare att leta upp nycklar i en hash-tabell än att filtrera en samling efter egenskapsvärden. I stället för att kontrollera varje objekt i samlingen kan PowerShell kontrollera om nyckeln har definierats och använda dess värde.
$Results = $Employees | ForEach-Object -Process {
$Email = $LookupHash[$_.Name].Email
[pscustomobject]@{
Id = $_.Id
Name = $_.Name
Email = $Email
}
}
Det här går mycket snabbare. Loopningsfiltret tog några minuter att slutföra, men hash-sökningen tar mindre än en sekund.
Använd Write-Host noggrant
Kommandot Write-Host
bör endast användas när du behöver skriva formaterad text till värdkonsolen i stället för att skriva objekt till pipelinen Lyckades .
Write-Host
kan vara en storleksordning som är långsammare än [Console]::WriteLine()
för specifika värdar som pwsh.exe
, powershell.exe
eller powershell_ise.exe
. Det är dock [Console]::WriteLine()
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
.
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 är klar ö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 kan du överväga att flytta loopen inuti funktionen.
Föreställ dig följande exempel:
$tests = @{
'Simple for-loop' = {
param([int] $RepeatCount, [random] $RanGen)
for ($i = 0; $i -lt $RepeatCount; $i++) {
$null = $RanGen.Next()
}
}
'Wrapped in a function' = {
param([int] $RepeatCount, [random] $RanGen)
function Get-RandomNumberCore {
param ($rng)
$rng.Next()
}
for ($i = 0; $i -lt $RepeatCount; $i++) {
$null = Get-RandomNumberCore -rng $RanGen
}
}
'for-loop in a function' = {
param([int] $RepeatCount, [random] $RanGen)
function Get-RandomNumberAll {
param ($rng, $count)
for ($i = 0; $i -lt $count; $i++) {
$null = $rng.Next()
}
}
Get-RandomNumberAll -rng $RanGen -count $RepeatCount
}
}
5kb, 10kb, 100kb | ForEach-Object {
$rng = [random]::new()
$groupResult = foreach ($test in $tests.GetEnumerator()) {
$ms = Measure-Command { & $test.Value -RepeatCount $_ -RanGen $rng }
[pscustomobject]@{
CollectionSize = $_
Test = $test.Key
TotalMilliseconds = [math]::Round($ms.TotalMilliseconds,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'
}
}
}
Basic for-loop-exemplet är baslinjen för prestanda. Det andra exemplet omsluter slumptalsgeneratorn i en funktion som anropas i en snäv loop. Det tredje exemplet flyttar loopen inuti funktionen. Funktionen anropas bara en gång, men koden genererar fortfarande samma mängd slumpmässiga tal. Observera skillnaden i körningstider för varje exempel.
CollectionSize Test TotalMilliseconds RelativeSpeed
-------------- ---- ----------------- -------------
5120 for-loop in a function 9.62 1x
5120 Simple for-loop 10.55 1.1x
5120 Wrapped in a function 62.39 6.49x
10240 Simple for-loop 17.79 1x
10240 for-loop in a function 18.48 1.04x
10240 Wrapped in a function 127.39 7.16x
102400 for-loop in a function 179.19 1x
102400 Simple for-loop 181.58 1.01x
102400 Wrapped in a function 1155.57 6.45x
Undvik att omsluta cmdlet-pipelines
De flesta cmdletar implementeras för pipelinen, vilket är en sekventiell syntax och process. Till exempel:
cmdlet1 | cmdlet2 | cmdlet3
Det kan vara dyrt att initiera en ny pipeline. Därför bör du undvika att omsluta en cmdlet-pipeline till en annan befintlig pipeline.
Betänk följande exempel. Filen Input.csv
innehåller 2 100 rader. Kommandot Export-Csv
omsluts i pipelinen ForEach-Object
. Cmdleten Export-Csv
anropas för varje iteration av loopen ForEach-Object
.
$measure = Measure-Command -Expression {
Import-Csv .\Input.csv | ForEach-Object -Begin { $Id = 1 } -Process {
[PSCustomObject]@{
Id = $Id
Name = $_.opened_by
} | Export-Csv .\Output1.csv -Append
}
}
'Wrapped = {0:N2} ms' -f $measure.TotalMilliseconds
Wrapped = 15,968.78 ms
I nästa exempel Export-Csv
flyttades kommandot utanför pipelinen ForEach-Object
.
I det här fallet Export-Csv
anropas bara en gång, men bearbetar fortfarande alla objekt som skickas ut från ForEach-Object
.
$measure = Measure-Command -Expression {
Import-Csv .\Input.csv | ForEach-Object -Begin { $Id = 2 } -Process {
[PSCustomObject]@{
Id = $Id
Name = $_.opened_by
}
} | Export-Csv .\Output2.csv
}
'Unwrapped = {0:N2} ms' -f $measure.TotalMilliseconds
Unwrapped = 42.92 ms
Det oöppnade exemplet är 372 gånger snabbare. Observera också att den första implementeringen kräver parametern Lägg till , vilket inte krävs för den senare implementeringen.
Relaterade länkar
Feedback
https://aka.ms/ContentUserFeedback.
Kommer snart: Under hela 2024 kommer vi att fasa ut GitHub-problem som feedbackmekanism för innehåll och ersätta det med ett nytt feedbacksystem. Mer information finns i:Skicka och visa feedback för