Delen via


Prestatieoverwegingen voor PowerShell-scripts

PowerShell-scripts die rechtstreeks gebruikmaken van .NET en voorkomen dat de pijplijn sneller is dan idiomatische PowerShell. Idiomatic PowerShell maakt gebruik van cmdlets en PowerShell-functies, vaak gebruikmakend van de pijplijn en gebruiken van .NET alleen wanneer dat nodig is.

Notitie

Veel van de hier beschreven technieken zijn geen idiomatische PowerShell en kunnen de leesbaarheid van een PowerShell-script verminderen. Auteurs van scripts wordt aangeraden om idiomatische PowerShell te gebruiken, tenzij de prestaties anders dicteren.

Uitvoer onderdrukken

Er zijn veel manieren om te voorkomen dat objecten naar de pijplijn worden geschreven.

  • Toewijzing of bestandsomleiding naar $null
  • Casten naar [void]
  • Sluis naar Out-Null

De snelheden van het toewijzen aan $null, casten naar [void]en het omleiden $null van bestanden zijn bijna identiek. Het aanroepen Out-Null in een grote lus kan echter aanzienlijk langzamer zijn, met name 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'
        }
    }
}

Deze tests werden uitgevoerd op een Windows 11-computer in PowerShell 7.3.4. De resultaten worden hieronder weergegeven:

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

De tijden en relatieve snelheden kunnen variëren, afhankelijk van de hardware, de versie van PowerShell en de huidige werkbelasting op het systeem.

Matrix toevoegen

Het genereren van een lijst met items wordt vaak gedaan met behulp van een matrix met de optellenoperator:

$results = @()
$results += Get-Something
$results += Get-SomethingElse
$results

Het toevoegen van matrices is inefficiënt omdat matrices een vaste grootte hebben. Elke toevoeging aan de matrix maakt een nieuwe matrix die groot genoeg is om alle elementen van zowel de linker- als rechteroperanden vast te houden. De elementen van beide operanden worden gekopieerd naar de nieuwe matrix. Voor kleine verzamelingen maakt deze overhead mogelijk niet uit. Prestaties kunnen leiden tot grote verzamelingen.

Er zijn een paar alternatieven. Als u geen matrix nodig hebt, kunt u in plaats daarvan een getypte algemene lijst gebruiken ([List<T>]):

$results = [System.Collections.Generic.List[object]]::new()
$results.AddRange((Get-Something))
$results.AddRange((Get-SomethingElse))
$results

De invloed op de prestaties van het gebruik van matrix toevoegen neemt exponentieel toe met de grootte van de verzameling en de aantal toevoegingen. Deze code vergelijkt expliciet het toewijzen van waarden aan een matrix met het toevoegen van matrices en het gebruik van de Add(T) methode voor een [List<T>] object. Het definieert expliciete toewijzing als de basislijn voor prestaties.

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

Deze tests werden uitgevoerd op een Windows 11-computer 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

Wanneer u met grote verzamelingen werkt, is het toevoegen van matrices aanzienlijk langzamer dan het toevoegen aan een List<T>.

Wanneer u een [List<T>] object gebruikt, moet u de lijst maken met een specifiek type, zoals [String] of [Int]. Wanneer u objecten van een ander type aan de lijst toevoegt, worden ze naar het opgegeven type gecast. Als ze niet naar het opgegeven type kunnen worden gecast, genereert de methode een uitzondering.

$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

Wanneer u de lijst nodig hebt om een verzameling van verschillende typen objecten te zijn, maakt u deze als [Object] lijsttype. U kunt de verzameling inventariseren welke typen objecten erin zijn opgenomen.

$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

Als u wel een matrix nodig hebt, kunt u de ToArray() methode in de lijst aanroepen of u kunt PowerShell de matrix voor u laten maken:

$results = @(
    Get-Something
    Get-SomethingElse
)

In dit voorbeeld maakt PowerShell een [ArrayList] bestand waarin de resultaten die naar de pijplijn in de matrixexpressie zijn geschreven, worden opgeslagen. Net voordat u deze $resultstoewijst, converteert PowerShell de [ArrayList] naar een [Object[]].

Optellen van tekenreeks

Tekenreeksen zijn onveranderbaar. Elke toevoeging aan de tekenreeks maakt daadwerkelijk een nieuwe tekenreeks die groot genoeg is om de inhoud van zowel de linker- als rechteroperanden op te slaan en kopieert vervolgens de elementen van beide operanden naar de nieuwe tekenreeks. Voor kleine tekenreeksen maakt deze overhead mogelijk niet uit. Voor grote tekenreeksen kan dit van invloed zijn op de prestaties en het geheugenverbruik.

Er zijn ten minste twee alternatieven:

  • De -join operator voegt tekenreeksen samen
  • De .NET-klasse [StringBuilder] biedt een veranderlijke tekenreeks

In het volgende voorbeeld worden de prestaties van deze drie methoden voor het bouwen van een tekenreeks vergeleken.

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

Deze tests zijn uitgevoerd op een Windows 11-computer in PowerShell 7.4.2. In de uitvoer ziet u dat de -join operator het snelst is, gevolgd door de [StringBuilder] klasse.

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

De tijden en relatieve snelheden kunnen variëren, afhankelijk van de hardware, de versie van PowerShell en de huidige werkbelasting op het systeem.

Grote bestanden verwerken

De idiomatische manier om een bestand te verwerken in PowerShell kan er ongeveer als volgt uitzien:

Get-Content $path | Where-Object Length -GT 10

Dit kan een orde van grootte langzamer zijn dan het rechtstreeks gebruiken van .NET-API's. U kunt bijvoorbeeld de .NET-klasse [StreamReader] gebruiken:

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()
    }
}

U kunt ook de ReadLines methode van [System.IO.File], die inpakt StreamReader, vereenvoudigt het leesproces:

foreach ($line in [System.IO.File]::ReadLines($path)) {
    if ($line.Length -gt 10) {
        $line
    }
}

Vermeldingen opzoeken op eigenschap in grote verzamelingen

Het is gebruikelijk om een gedeelde eigenschap te gebruiken om dezelfde record in verschillende verzamelingen te identificeren, zoals het gebruik van een naam om een id op te halen uit de ene lijst en een e-mailbericht van een andere. Het herhalen van de eerste lijst om de overeenkomende record in de tweede verzameling te vinden, is traag. Met name de herhaalde filtering van de tweede verzameling heeft een grote overhead.

Gegeven twee verzamelingen, één met een id en naam, de andere met naam en e-mail:

$Employees = 1..10000 | ForEach-Object {
    [PSCustomObject]@{
        Id   = $_
        Name = "Name$_"
    }
}

$Accounts = 2500..7500 | ForEach-Object {
    [PSCustomObject]@{
        Name  = "Name$_"
        Email = "Name$_@fabrikam.com"
    }
}

De gebruikelijke manier om deze verzamelingen af te stemmen om een lijst met objecten te retourneren met de eigenschappen ID, Naam en E-mail , kan er als volgt uitzien:

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

Deze implementatie moet echter alle 5000 items in de $Accounts verzameling één keer filteren voor elk item in de $Employee verzameling. Dat kan minuten duren, zelfs voor deze zoekactie met één waarde.

In plaats daarvan kunt u een Hash-tabel maken die gebruikmaakt van de eigenschap Gedeelde naam als sleutel en het overeenkomende account als de waarde.

$LookupHash = @{}
foreach ($Account in $Accounts) {
    $LookupHash[$Account.Name] = $Account
}

Het opzoeken van sleutels in een hash-tabel is veel sneller dan het filteren van een verzameling op eigenschapswaarden. In plaats van elk item in de verzameling te controleren, kan PowerShell controleren of de sleutel is gedefinieerd en de waarde ervan gebruiken.

$Results = $Employees | ForEach-Object -Process {
    $Email = $LookupHash[$_.Name].Email
    [pscustomobject]@{
        Id    = $_.Id
        Name  = $_.Name
        Email = $Email
    }
}

Dit is veel sneller. Tijdens het voltooien van het lusfilter duurt het minder dan een seconde om de hash-zoekactie te voltooien.

Schrijfhost zorgvuldig gebruiken

De Write-Host opdracht mag alleen worden gebruikt wanneer u opgemaakte tekst naar de hostconsole moet schrijven in plaats van objecten te schrijven naar de succespijplijn .

Write-Host kan een orde van grootte langzamer zijn dan [Console]::WriteLine() voor specifieke hosts, zoals pwsh.exe, powershell.exeof powershell_ise.exe. [Console]::WriteLine() Het is echter niet gegarandeerd dat ze op alle hosts werken. Uitvoer die wordt geschreven met behulp [Console]::WriteLine() van, wordt ook niet geschreven naar transcripties gestart.Start-Transcript

JIT-compilatie

PowerShell compileert de scriptcode naar bytecode die wordt geïnterpreteerd. Vanaf PowerShell 3 kan PowerShell voor code die herhaaldelijk in een lus wordt uitgevoerd, de prestaties verbeteren door Just-In-Time (JIT) de code te compileren in systeemeigen code.

Lussen met minder dan 300 instructies komen in aanmerking voor JIT-compilatie. Lussen die te kostbaar zijn om te compileren. Wanneer de lus 16 keer is uitgevoerd, wordt het script op de achtergrond gecompileerd met JIT. Wanneer de JIT-compilatie is voltooid, wordt de uitvoering overgebracht naar de gecompileerde code.

Vermijd herhaalde aanroepen naar een functie

Het aanroepen van een functie kan een dure bewerking zijn. Als u een functie aanroept in een langdurige strakke lus, kunt u overwegen de lus binnen de functie te verplaatsen.

Bekijk de volgende voorbeelden:

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

Het basic for-loop-voorbeeld is de basislijn voor prestaties. In het tweede voorbeeld wordt de generator voor willekeurige getallen verpakt in een functie die in een strakke lus wordt aangeroepen. In het derde voorbeeld wordt de lus binnen de functie verplaatst. De functie wordt slechts eenmaal aangeroepen, maar de code genereert nog steeds dezelfde hoeveelheid willekeurige getallen. Let op het verschil in uitvoeringstijden voor elk voorbeeld.

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

Wrapping-cmdlet-pijplijnen voorkomen

De meeste cmdlets worden geïmplementeerd voor de pijplijn. Dit is een opeenvolgende syntaxis en een proces. Voorbeeld:

cmdlet1 | cmdlet2 | cmdlet3

Het initialiseren van een nieuwe pijplijn kan duur zijn, daarom moet u voorkomen dat u een cmdlet-pijplijn in een andere bestaande pijplijn verpakt.

Bekijk het volgende voorbeeld. Het Input.csv bestand bevat 2100 regels. De Export-Csv opdracht wordt verpakt in de ForEach-Object pijplijn. De Export-Csv cmdlet wordt aangeroepen voor elke iteratie van de ForEach-Object lus.

$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

In het volgende voorbeeld is de Export-Csv opdracht buiten de ForEach-Object pijplijn verplaatst. In dit geval Export-Csv wordt slechts één keer aangeroepen, maar worden nog steeds alle objecten verwerkt die zijn ForEach-Objectverstreken.

$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

Het uitgepakte voorbeeld is 372 keer sneller. U ziet ook dat voor de eerste implementatie de parameter Toevoegen is vereist. Deze parameter is niet vereist voor de latere implementatie.

OrderedDictionary gebruiken om dynamisch nieuwe objecten te maken

Er zijn situaties waarin we mogelijk dynamisch objecten moeten maken op basis van bepaalde invoer, de meest gebruikte manier om een nieuw PSObject te maken en vervolgens nieuwe eigenschappen toe te voegen met behulp van de Add-Member cmdlet. De prestatiekosten voor kleine verzamelingen die deze techniek gebruiken, kunnen verwaarloosbaar zijn, maar het kan zeer merkbaar worden voor grote verzamelingen. In dat geval is de aanbevolen methode om een [OrderedDictionary] en deze vervolgens te converteren naar een PSObject met behulp van de [pscustomobject] typeversneller. Zie de sectie Geordende woordenlijsten maken van about_Hash_Tables voor meer informatie.

Stel dat u het volgende API-antwoord hebt opgeslagen in de variabele $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" ]
      ]
    }
  ]
}

Stel dat u deze gegevens wilt exporteren naar een CSV. Eerst moet u nieuwe objecten maken en de eigenschappen en waarden toevoegen met behulp van de Add-Member cmdlet.

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

Met behulp van een OrderedDictionary, kan de code worden vertaald naar:

$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 beide gevallen zou de $result uitvoer hetzelfde zijn:

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

De laatste benadering wordt exponentieel efficiënter naarmate het aantal objecten en lideigenschappen toeneemt.

Hier volgt een prestatievergelijking van drie technieken voor het maken van objecten met 5 eigenschappen:

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

En dit zijn de resultaten:

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