Considerazioni sulle prestazioni di scripting di PowerShell
Gli script di PowerShell che sfruttano direttamente .NET ed evitano che la pipeline sia più veloce di PowerShell idiotica. PowerShell idiomatico usa cmdlet e funzioni di PowerShell, spesso sfruttando la pipeline e usando .NET solo quando necessario.
Nota
Molte delle tecniche descritte di seguito non sono idiotiche di PowerShell e possono ridurre la leggibilità di uno script di PowerShell. Gli autori di script sono invitati a usare PowerShell idiomatico, a meno che le prestazioni non determinino diversamente.
Eliminazione dell'output
Esistono molti modi per evitare di scrivere oggetti nella pipeline.
- Assegnazione a
$null
- Cast a
[void]
- Reindirizzamento di file a
$null
- Pipe a
Out-Null
La velocità di assegnazione a $null
, il cast a [void]
e il reindirizzamento dei file a $null
sono quasi identici. Tuttavia, la chiamata Out-Null
in un ciclo di grandi dimensioni può essere notevolmente più lenta, soprattutto in 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'
}
}
}
Questi test sono stati eseguiti in un computer Windows 11 in PowerShell 7.3.4. Di seguito sono riportati i risultati:
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
I tempi e le velocità relative possono variare a seconda dell'hardware, della versione di PowerShell e del carico di lavoro corrente nel sistema.
Aggiunta di matrici
La generazione di un elenco di elementi viene spesso eseguita usando una matrice con l'operatore di addizione:
$results = @()
$results += Do-Something
$results += Do-SomethingElse
$results
L'aggiunta di matrici non è efficiente perché le matrici hanno una dimensione fissa. Ogni aggiunta alla matrice crea una nuova matrice sufficientemente grande da contenere tutti gli elementi degli operandi sinistro e destro. Gli elementi di entrambi gli operandi vengono copiati nella nuova matrice. Per le raccolte di piccole dimensioni, questo sovraccarico potrebbe non essere importante. Le prestazioni possono risentire delle raccolte di grandi dimensioni.
Ci sono un paio di alternative. Se in realtà non è necessaria una matrice, prendere in considerazione l'uso di un elenco generico tipizzato (elenco<T>):
$results = [System.Collections.Generic.List[object]]::new()
$results.AddRange((Do-Something))
$results.AddRange((Do-SomethingElse))
$results
L'impatto sulle prestazioni dell'uso dell'aggiunta della matrice aumenta in modo esponenziale con le dimensioni della raccolta e le aggiunte di numeri. Questo codice confronta in modo esplicito l'assegnazione di valori a una matrice con l'aggiunta di matrici e l'uso del Add()
metodo in un elenco<T>. Definisce l'assegnazione esplicita come baseline per le prestazioni.
$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'
}
}
}
Questi test sono stati eseguiti in un computer Windows 11 in 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
Quando si lavora con raccolte di grandi dimensioni, l'aggiunta di matrici è notevolmente più lenta rispetto all'aggiunta a un elenco<T>.
Quando si usa un elenco<T>, è necessario creare l'elenco con un tipo specifico, ad esempio String o Int. Quando si aggiungono oggetti di un tipo diverso all'elenco, viene eseguito il cast al tipo specificato. Se non è possibile eseguire il cast al tipo specificato, il metodo genera un'eccezione.
$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
Quando è necessario che l'elenco sia una raccolta di diversi tipi di oggetti, crearlo con Object come tipo di elenco. È possibile enumerare l'insieme per esaminare i tipi degli oggetti in esso contenuti.
$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
Se è necessaria una matrice, è possibile chiamare il ToArray()
metodo nell'elenco oppure consentire a PowerShell di creare automaticamente la matrice:
$results = @(
Do-Something
Do-SomethingElse
)
In questo esempio PowerShell crea un oggetto ArrayList per contenere i risultati scritti nella pipeline all'interno dell'espressione di matrice. Poco prima dell'assegnazione a , PowerShell converte ArrayList in un oggetto[].$results
Addizione di stringhe
Le stringhe non sono modificabili. Ogni aggiunta alla stringa crea effettivamente una nuova stringa sufficientemente grande da contenere il contenuto degli operandi sinistro e destro, quindi copia gli elementi di entrambi gli operandi nella nuova stringa. Per le stringhe di piccole dimensioni, questo sovraccarico potrebbe non essere importante. Per stringhe di grandi dimensioni, questo può influire sull'utilizzo di prestazioni e memoria.
Esistono almeno due alternative:
- L'operatore
-join
concatena le stringhe - La classe StringBuilder .NET fornisce una stringa modificabile
Nell'esempio seguente vengono confrontate le prestazioni di questi tre metodi di compilazione di una stringa.
$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'
}
}
}
Questi test sono stati eseguiti in un computer Windows 10 in PowerShell 7.3.4. L'output mostra che l'operatore -join
è il più veloce, seguito dalla classe 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
I tempi e le velocità relative possono variare a seconda dell'hardware, della versione di PowerShell e del carico di lavoro corrente nel sistema.
Elaborazione di file di grandi dimensioni
Il modo idiotico per elaborare un file in PowerShell potrebbe essere simile al seguente:
Get-Content $path | Where-Object { $_.Length -gt 10 }
Questo può essere un ordine di grandezza più lento rispetto all'uso diretto delle API .NET:
try
{
$stream = [System.IO.StreamReader]::new($path)
while ($line = $stream.ReadLine())
{
if ($line.Length -gt 10)
{
$line
}
}
}
finally
{
$stream.Dispose()
}
Ricerca di voci per proprietà in raccolte di grandi dimensioni
È comune usare una proprietà condivisa per identificare lo stesso record in raccolte diverse, ad esempio usando un nome per recuperare un ID da un elenco e un messaggio di posta elettronica da un altro. L'iterazione del primo elenco per trovare il record corrispondente nella seconda raccolta è lento. In particolare, il filtro ripetuto della seconda raccolta presenta un sovraccarico elevato.
Date due raccolte, una con ID e Name, l'altra con Name e Email:
$Employees = 1..10000 | ForEach-Object {
[PSCustomObject]@{
Id = $_
Name = "Name$_"
}
}
$Accounts = 2500..7500 | ForEach-Object {
[PSCustomObject]@{
Name = "Name$_"
Email = "Name$_@fabrikam.com"
}
}
Il modo consueto per riconciliare queste raccolte per restituire un elenco di oggetti con le proprietà ID, Name e Email potrebbe essere simile al seguente:
$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
}
}
Tuttavia, tale implementazione deve filtrare tutti i 5000 elementi nella $Accounts
raccolta una volta per ogni elemento della $Employee
raccolta. Questa operazione può richiedere minuti, anche per questa ricerca a valore singolo.
È invece possibile creare una tabella hash che usa la proprietà Shared Name come chiave e l'account corrispondente come valore.
$LookupHash = @{}
foreach ($Account in $Accounts) {
$LookupHash[$Account.Name] = $Account
}
La ricerca di chiavi in una tabella hash è molto più veloce rispetto al filtro di una raccolta in base ai valori delle proprietà. Anziché controllare ogni elemento nella raccolta, PowerShell può controllare se la chiave è definita e usarne il valore.
$Results = $Employees | ForEach-Object -Process {
$Email = $LookupHash[$_.Name].Email
[pscustomobject]@{
Id = $_.Id
Name = $_.Name
Email = $Email
}
}
Questo è molto più veloce. Durante il completamento del filtro di ciclo sono necessari minuti, la ricerca hash richiede meno di un secondo.
Evitare write-host
In genere è considerato poco pratico scrivere l'output direttamente nella console, ma quando ha senso, molti script usano Write-Host
.
Se è necessario scrivere molti messaggi nella console, Write-Host
può essere un ordine di grandezza più lento rispetto [Console]::WriteLine()
a per host specifici, ad pwsh.exe
esempio , powershell.exe
o powershell_ise.exe
. Tuttavia, [Console]::WriteLine()
non è garantito il funzionamento in tutti gli host. Inoltre, l'output scritto con [Console]::WriteLine()
non viene scritto nelle trascrizioni avviate da Start-Transcript
.
Invece di usare Write-Host
, è consigliabile usare Write-Output.
Compilazione JIT
PowerShell compila il codice script in bytecode interpretato. A partire da PowerShell 3, per il codice eseguito ripetutamente in un ciclo, PowerShell può migliorare le prestazioni compilando il codice in codice nativo.
I cicli con meno di 300 istruzioni sono idonei per la compilazione JIT. Cicli più grandi di quelli troppo costosi per la compilazione. Quando il ciclo è stato eseguito 16 volte, lo script viene compilato in background. Al termine della compilazione JIT, l'esecuzione viene trasferita al codice compilato.
Evitare chiamate ripetute a una funzione
La chiamata a una funzione può essere un'operazione costosa. Se si chiama una funzione in un ciclo stretto a esecuzione prolungata, è consigliabile spostare il ciclo all'interno della funzione.
Vedi gli esempi seguenti:
$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
L'esempio di ciclo for basic è la linea di base per le prestazioni. Il secondo esempio esegue il wrapping del generatore di numeri casuali in una funzione chiamata in un ciclo stretto. Il terzo esempio sposta il ciclo all'interno della funzione. La funzione viene chiamata una sola volta, ma il codice genera ancora 10000 numeri casuali. Si noti la differenza nei tempi di esecuzione per ogni esempio.
Basic for-loop = 47.8668ms
Wrapped in a function = 820.1396ms
For-loop in a function = 23.3193ms
Evitare di eseguire il wrapping delle pipeline dei cmdlet
La maggior parte dei cmdlet viene implementata per la pipeline, ovvero una sintassi e un processo sequenziali. Ad esempio:
cmdlet1 | cmdlet2 | cmdlet3
L'inizializzazione di una nuova pipeline può risultare costosa, pertanto è consigliabile evitare di eseguire il wrapping di una pipeline di cmdlet in un'altra pipeline esistente.
Si consideri l'esempio seguente. Il Input.csv
file contiene 2100 righe. Il Export-Csv
comando viene sottoposto a wrapping all'interno della ForEach-Object
pipeline. Il Export-Csv
cmdlet viene richiamato per ogni iterazione del ForEach-Object
ciclo.
'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
Per l'esempio successivo, il Export-Csv
comando è stato spostato all'esterno della ForEach-Object
pipeline.
In questo caso, Export-Csv
viene richiamato una sola volta, ma elabora comunque tutti gli oggetti passati da 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
L'esempio non sottoposto a wrapping è 372 volte più veloce. Si noti anche che la prima implementazione richiede il parametro Append , che non è necessario per l'implementazione successiva.
Commenti e suggerimenti
https://aka.ms/ContentUserFeedback.
Presto disponibile: Nel corso del 2024 verranno gradualmente disattivati i problemi di GitHub come meccanismo di feedback per il contenuto e ciò verrà sostituito con un nuovo sistema di feedback. Per altre informazioni, vedereInvia e visualizza il feedback per