Dela via


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.

Anteckning

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.

Undertrycka utdata

Det finns många sätt att undvika att skriva objekt till pipelinen.

  • Tilldelning eller filomdirigering till $null
  • Sända till [void]
  • Rör till Out-Null

Hastigheterna för att tilldela 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.

Array-tillä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

Anteckning

I PowerShell 7.5 optimerades matristillägget och skapar inte längre en ny matris för varje åtgärd. De prestandaöverväganden som beskrivs här gäller fortfarande för PowerShell-versioner före 7.5. Mer information finns i Nyheter i PowerShell 7.5.

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 arraytillägg växer exponentiellt med storleken på samlingen och antalet tillägg. Den här koden jämför explicit tilldelning av värden till en matris med att använda matristillägg och använda metoden Add(T) på 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 det betydligt långsammare att lägga till i en array ä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 metoden ToArray() 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 $resultskonverterar PowerShell [ArrayList] 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
  • Klassen .NET [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 klassen [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

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 tiopotens långsammare än att använda .NET-API:er direkt. Du kan till exempel använda klassen .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()
    }
}

Du kan också använda metoden ReadLines för [System.IO.File], 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. Att iterera över den första listan för att hitta den matchande posten i den andra samlingen är långsamt. I synnerhet har den upprepade filtreringen av den andra samlingen stora omkostnader.

Med två samlingar, en med ett -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, Nameoch Email 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 samling en gång för varje objekt i $Employee-samlingen. Det kan ta minuter, även för denna enkelvärdesuppslagning.

I stället kan du skapa en Hash-tabell som använder den delade egenskapen Name 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.exeeller powershell_ise.exe. Men [Console]::WriteLine() är inte garanterat att fungera 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 långkörande snäv loop, kan du överväga att flytta loopen inuti funktionen.

Tänk på 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'
        }
    }
}

Exemplet Basic for-loop ä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 exekveringstider 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.

Tänk dig följande exempel. Filen Input.csv innehåller 2 100 rader. Kommandot Export-Csv är inneslutet i den ForEach-Object-pipeline. Cmdleten Export-Csv anropas för varje iteration av ForEach-Object-loopen.

$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 flyttades kommandot Export-Csv utanför ForEach-Object pipeline. I det här fallet anropas Export-Csv 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.

Skapa objekt

Det kan ta lång tid att skapa objekt med hjälp av cmdleten New-Object. Följande kod jämför prestandan för att skapa objekt med hjälp av cmdleten New-Object med [pscustomobject] typaccelerator.

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 har lagt till den statiska metoden new() för alla .NET-typer. Följande kod jämför prestandan för att skapa objekt med hjälp av cmdleten New-Object med metoden 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

Använd OrderedDictionary för att dynamiskt skapa nya objekt

Det finns situationer där vi kan behöva skapa objekt dynamiskt baserat på vissa indata, det kanske vanligaste sättet att skapa en ny PSObject- och sedan lägga till nya egenskaper med hjälp av cmdleten Add-Member. Prestandakostnaden för små samlingar med den här tekniken kan vara försumbar, men den kan bli mycket märkbar för stora samlingar. I så fall är den rekommenderade metoden att använda en [OrderedDictionary] och sedan konvertera den till en PSObject- med hjälp av [pscustomobject] typaccelerator. Mer information finns i avsnittet Skapa ordnade ordlistor i about_Hash_Tables.

Anta att du har följande API-svar lagrat i variabeln $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" ]
      ]
    }
  ]
}

Anta nu att du vill exportera dessa data till en CSV. Först måste du skapa nya objekt och lägga till egenskaper och värden med hjälp av cmdleten 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
}

Med hjälp av en OrderedDictionarykan koden översättas till:

$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
}

I båda fallen skulle $result utdata vara desamma:

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

Den senare metoden blir exponentiellt effektivare när antalet objekt och medlemsegenskaper ökar.

Här är en prestandajämförelse av tre tekniker för att skapa objekt med 5 egenskaper:

$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'
        }
    }
}

Och det här är resultatet:

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