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 o reindirizzamento di file a
$null
- Cast in
[void]
- Pipe a
Out-Null
Le velocità di assegnazione a $null
, il cast a [void]
e il reindirizzamento dei file a $null
sono quasi identici. Tuttavia, la chiamata di 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 += Get-Something
$results += Get-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 ([List<T>]
):
$results = [System.Collections.Generic.List[object]]::new()
$results.AddRange((Get-Something))
$results.AddRange((Get-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 metodo Add(T)
su un oggetto [List<T>]
. Definisce l'assegnazione esplicita come baseline per le prestazioni.
$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'
}
}
}
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(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
Quando si lavora con raccolte di grandi dimensioni, l'aggiunta di matrici è notevolmente più lenta rispetto all'aggiunta a un List<T>
.
Quando si usa un oggetto [List<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 | ForEach-Object { "$_ is $($_.GetType())" }
1 is int
2 is string
3 is double
Se è necessaria una matrice, è possibile chiamare il metodo ToArray()
nell'elenco oppure consentire a PowerShell di creare automaticamente la matrice:
$results = @(
Get-Something
Get-SomethingElse
)
In questo esempio PowerShell crea un [ArrayList]
per contenere i risultati scritti nella pipeline all'interno dell'espressione di matrice. Poco prima di assegnare a $results
, PowerShell converte il [ArrayList]
in un [Object[]]
.
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 stringhe - La classe .NET
[StringBuilder]
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 11 in PowerShell 7.4.2. L'output mostra che l'operatore -join
è il più veloce, seguito dalla classe [StringBuilder]
.
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
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. Ad esempio, è possibile usare la classe .NET [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()
}
}
È anche possibile usare il metodo ReadLines
di [System.IO.File]
, che esegue il wrapping di StreamReader
, semplifica il processo di lettura:
foreach ($line in [System.IO.File]::ReadLines($path)) {
if ($line.Length -gt 10) {
$line
}
}
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 un ID
$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 l'ID , Namee le proprietà Email potrebbero essere simili alle seguenti:
$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 raccolta $Accounts
una volta per ogni elemento della raccolta $Employee
. Questa operazione può richiedere minuti, anche per questa ricerca a valore singolo.
È invece possibile creare un
$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.
Usare attentamente Write-Host
Il comando Write-Host
deve essere usato solo quando è necessario scrivere testo formattato nella console host, anziché scrivere oggetti nella pipeline Operazione riuscita.
Write-Host
può essere un ordine di grandezza più lento di [Console]::WriteLine()
per host specifici, ad esempio pwsh.exe
, 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
.
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.
Si considerino gli esempi seguenti:
$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'
}
}
}
L'esempio
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
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. Per 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 file Input.csv
contiene 2100 righe. Il comando Export-Csv
viene eseguito il wrapping all'interno della pipeline di ForEach-Object
. Il cmdlet Export-Csv
viene richiamato per ogni iterazione del ciclo 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
Per l'esempio successivo, il comando Export-Csv
è stato spostato all'esterno della pipeline di ForEach-Object
.
In questo caso, Export-Csv
viene richiamato una sola volta, ma elabora comunque tutti gli oggetti passati da 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
L'esempio non sottoposto a wrapping è 372 volte più veloce. Si noti anche che la prima implementazione richiede il parametro Append
Creazione di oggetti
La creazione di oggetti tramite il cmdlet New-Object
può risultare lenta. Il codice seguente confronta le prestazioni della creazione di oggetti usando il cmdlet New-Object
con l'acceleratore di tipo [pscustomobject]
.
Measure-Command {
$test = 'PSCustomObject'
for ($i = 0; $i -lt 100000; $i++) {
$resultObject = [PSCustomObject]@{
Name = 'Name'
Path = 'FullName'
}
}
} | Select-Object @{n='Test';e={$test}},TotalSeconds
Measure-Command {
$test = 'New-Object'
for ($i = 0; $i -lt 100000; $i++) {
$resultObject = New-Object -TypeName PSObject -Property @{
Name = 'Name'
Path = 'FullName'
}
}
} | Select-Object @{n='Test';e={$test}},TotalSeconds
Test TotalSeconds
---- ------------
PSCustomObject 0.48
New-Object 3.37
PowerShell 5.0 ha aggiunto il metodo statico new()
per tutti i tipi .NET. Il codice seguente confronta le prestazioni della creazione di oggetti usando il cmdlet New-Object
con il metodo new()
.
Measure-Command {
$test = 'new() method'
for ($i = 0; $i -lt 100000; $i++) {
$sb = [System.Text.StringBuilder]::new(1000)
}
} | Select-Object @{n='Test';e={$test}},TotalSeconds
Measure-Command {
$test = 'New-Object'
for ($i = 0; $i -lt 100000; $i++) {
$sb = New-Object -TypeName System.Text.StringBuilder -ArgumentList 1000
}
} | Select-Object @{n='Test';e={$test}},TotalSeconds
Test TotalSeconds
---- ------------
new() method 0.59
New-Object 3.17
Usare OrderedDictionary per creare dinamicamente nuovi oggetti
In alcune situazioni potrebbe essere necessario creare oggetti in modo dinamico in base ad alcuni input, il modo forse più comunemente usato per creare un nuovo PSObject e quindi aggiungere nuove proprietà usando il cmdlet Add-Member
. Il costo delle prestazioni per le raccolte di piccole dimensioni che usano questa tecnica può essere trascurabile, ma può diventare molto evidente per le raccolte di grandi dimensioni. In tal caso, l'approccio consigliato consiste nell'usare un [OrderedDictionary]
e quindi convertirlo in un PSObject usando l'acceleratore di tipo [pscustomobject]
. Per altre informazioni, vedere la sezione Creazione di dizionari ordinati di about_Hash_Tables.
Si supponga di avere la risposta API seguente archiviata nella variabile $json
.
{
"tables": [
{
"name": "PrimaryResult",
"columns": [
{ "name": "Type", "type": "string" },
{ "name": "TenantId", "type": "string" },
{ "name": "count_", "type": "long" }
],
"rows": [
[ "Usage", "63613592-b6f7-4c3d-a390-22ba13102111", "1" ],
[ "Usage", "d436f322-a9f4-4aad-9a7d-271fbf66001c", "1" ],
[ "BillingFact", "63613592-b6f7-4c3d-a390-22ba13102111", "1" ],
[ "BillingFact", "d436f322-a9f4-4aad-9a7d-271fbf66001c", "1" ],
[ "Operation", "63613592-b6f7-4c3d-a390-22ba13102111", "7" ],
[ "Operation", "d436f322-a9f4-4aad-9a7d-271fbf66001c", "5" ]
]
}
]
}
Si supponga ora di voler esportare questi dati in un file CSV. Prima di tutto è necessario creare nuovi oggetti e aggiungere le proprietà e i valori usando il cmdlet Add-Member
.
$data = $json | ConvertFrom-Json
$columns = $data.tables.columns
$result = foreach ($row in $data.tables.rows) {
$obj = [psobject]::new()
$index = 0
foreach ($column in $columns) {
$obj | Add-Member -MemberType NoteProperty -Name $column.name -Value $row[$index++]
}
$obj
}
Usando un OrderedDictionary
, il codice può essere convertito in:
$data = $json | ConvertFrom-Json
$columns = $data.tables.columns
$result = foreach ($row in $data.tables.rows) {
$obj = [ordered]@{}
$index = 0
foreach ($column in $columns) {
$obj[$column.name] = $row[$index++]
}
[pscustomobject] $obj
}
In entrambi i casi l'output $result
sarà lo stesso:
Type TenantId count_
---- -------- ------
Usage 63613592-b6f7-4c3d-a390-22ba13102111 1
Usage d436f322-a9f4-4aad-9a7d-271fbf66001c 1
BillingFact 63613592-b6f7-4c3d-a390-22ba13102111 1
BillingFact d436f322-a9f4-4aad-9a7d-271fbf66001c 1
Operation 63613592-b6f7-4c3d-a390-22ba13102111 7
Operation d436f322-a9f4-4aad-9a7d-271fbf66001c 5
Quest'ultimo approccio diventa esponenzialmente più efficiente man mano che aumenta il numero di oggetti e proprietà membro.
Ecco un confronto delle prestazioni di tre tecniche per la creazione di oggetti con 5 proprietà:
$tests = @{
'[ordered] into [pscustomobject] cast' = {
param([int] $iterations, [string[]] $props)
foreach ($i in 1..$iterations) {
$obj = [ordered]@{}
foreach ($prop in $props) {
$obj[$prop] = $i
}
[pscustomobject] $obj
}
}
'Add-Member' = {
param([int] $iterations, [string[]] $props)
foreach ($i in 1..$iterations) {
$obj = [psobject]::new()
foreach ($prop in $props) {
$obj | Add-Member -MemberType NoteProperty -Name $prop -Value $i
}
$obj
}
}
'PSObject.Properties.Add' = {
param([int] $iterations, [string[]] $props)
# this is how, behind the scenes, `Add-Member` attaches
# new properties to our PSObject.
# Worth having it here for performance comparison
foreach ($i in 1..$iterations) {
$obj = [psobject]::new()
foreach ($prop in $props) {
$obj.PSObject.Properties.Add(
[psnoteproperty]::new($prop, $i))
}
$obj
}
}
}
$properties = 'Prop1', 'Prop2', 'Prop3', 'Prop4', 'Prop5'
1kb, 10kb, 100kb | ForEach-Object {
$groupResult = foreach ($test in $tests.GetEnumerator()) {
$ms = Measure-Command { & $test.Value -iterations $_ -props $properties }
[pscustomobject]@{
Iterations = $_
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'
}
}
}
E questi sono i risultati:
Iterations Test TotalMilliseconds RelativeSpeed
---------- ---- ----------------- -------------
1024 [ordered] into [pscustomobject] cast 22.00 1x
1024 PSObject.Properties.Add 153.17 6.96x
1024 Add-Member 261.96 11.91x
10240 [ordered] into [pscustomobject] cast 65.24 1x
10240 PSObject.Properties.Add 1293.07 19.82x
10240 Add-Member 2203.03 33.77x
102400 [ordered] into [pscustomobject] cast 639.83 1x
102400 PSObject.Properties.Add 13914.67 21.75x
102400 Add-Member 23496.08 36.72x