Freigeben über


Überlegungen zur Leistung von PowerShell-Skripts

PowerShell-Skripts, die .NET direkt nutzen und die Pipeline vermeiden, sind tendenziell schneller als idiomatische PowerShell. Idiomatische PowerShell verwendet Cmdlets und PowerShell-Funktionen, die häufig die Pipeline nutzen und nur bei Bedarf auf .NET zurückgreifen.

Anmerkung

Viele der hier beschriebenen Techniken sind keine idiomatische PowerShell und können die Lesbarkeit eines PowerShell-Skripts verringern. Skriptautoren werden empfohlen, idiomatische PowerShell zu verwenden, es sei denn, die Leistung diktiert andernfalls.

Ausgabe unterdrücken

Es gibt viele Möglichkeiten, das Schreiben von Objekten in die Pipeline zu vermeiden.

  • Zuordnung oder Dateiumleitung zu $null
  • Umwandlung in [void]
  • Rohr an Out-Null

Die Geschwindigkeiten der Zuordnung zu $null, Umwandlung zu [void]und Dateiumleitung zu $null sind nahezu identisch. Das Aufrufen von Out-Null in einer großen Schleife kann jedoch erheblich langsamer sein, insbesondere 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'
        }
    }
}

Diese Tests wurden auf einem Windows 11-Computer in PowerShell 7.3.4 ausgeführt. Die Ergebnisse werden unten angezeigt:

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

Die Zeiten und relativen Geschwindigkeiten können je nach Hardware, Version von PowerShell und der aktuellen Workload auf dem System variieren.

Arrayzugabe

Das Generieren einer Liste von Elementen erfolgt häufig mithilfe eines Arrays mit dem Additionsoperator:

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

Die Arrayzugabe ist ineffizient, da Arrays eine feste Größe aufweisen. Jede Ergänzung des Arrays erstellt ein neues Array, das groß genug ist, um alle Elemente der linken und rechten Operanden zu enthalten. Die Elemente beider Operanden werden in das neue Array kopiert. Bei kleinen Sammlungen spielt dieser Aufwand möglicherweise keine Rolle. Die Leistung kann für große Sammlungen leiden.

Es gibt eine Reihe von Alternativen. Wenn Sie kein Array benötigen, sollten Sie stattdessen eine typierte generische Liste verwenden ([List<T>]):

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

Die Leistungsauswirkung der Verwendung der Arrayzugabe wächst exponentiell mit der Größe der Sammlung und den Zahlenzugaben. Mit diesem Code wird das explizite Zuweisen von Werten zu einem Array mit der Verwendung des Arrays und die Verwendung der Add(T)-Methode für ein [List<T>]-Objekt verglichen. Sie definiert die explizite Zuordnung als Basisplan für die Leistung.

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

Diese Tests wurden auf einem Windows 11-Computer in PowerShell 7.3.4 ausgeführt.

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

Wenn Sie mit großen Sammlungen arbeiten, ist das Hinzufügen von Arrays erheblich langsamer als das Hinzufügen zu einer List<T>.

Wenn Sie ein [List<T>]-Objekt verwenden, müssen Sie die Liste mit einem bestimmten Typ erstellen, z. B. [String] oder [Int]. Wenn Sie der Liste Objekte eines anderen Typs hinzufügen, werden sie in den angegebenen Typ umgegossen. Wenn sie nicht in den angegebenen Typ umwandeln können, löst die Methode eine Ausnahme aus.

$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

Wenn die Liste eine Sammlung verschiedener Objekttypen sein muss, erstellen Sie sie mit [Object] als Listentyp. Sie können die Auflistung auflisten, um die Typen der darin enthaltenen Objekte zu prüfen.

$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

Wenn Sie ein Array benötigen, können Sie die ToArray()-Methode in der Liste aufrufen oder PowerShell das Array für Sie erstellen lassen:

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

In diesem Beispiel erstellt PowerShell eine [ArrayList], um die Ergebnisse in der Pipeline innerhalb des Arrayausdrucks zu enthalten. Bevor Sie $resultszuweisen, konvertiert PowerShell die [ArrayList] in eine [Object[]].

Zeichenfolgenzugabe

Zeichenfolgen sind unveränderlich. Jede Ergänzung zur Zeichenfolge erstellt tatsächlich eine neue Zeichenfolge, die groß genug ist, um den Inhalt der linken und rechten Operanden zu halten, und kopiert dann die Elemente beider Operanden in die neue Zeichenfolge. Bei kleinen Zeichenfolgen spielt dieser Aufwand möglicherweise keine Rolle. Bei großen Zeichenfolgen kann sich dies auf die Leistung und den Arbeitsspeicherverbrauch auswirken.

Es gibt mindestens zwei Alternativen:

  • Der -join-Operator verkettet Zeichenfolgen
  • Die .NET-[StringBuilder]-Klasse stellt eine veränderbare Zeichenfolge bereit.

Im folgenden Beispiel wird die Leistung dieser drei Methoden zum Erstellen einer Zeichenfolge verglichen.

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

Diese Tests wurden auf einem Windows 11-Computer in PowerShell 7.4.2 ausgeführt. Die Ausgabe zeigt, dass der -join-Operator am schnellsten ist, gefolgt von der [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

Die Zeiten und relativen Geschwindigkeiten können je nach Hardware, Version von PowerShell und der aktuellen Workload auf dem System variieren.

Verarbeiten großer Dateien

Die idiomatische Methode zum Verarbeiten einer Datei in PowerShell könnte etwa wie folgt aussehen:

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

Dies kann eine Größenordnung langsamer sein als .NET-APIs direkt zu verwenden. Sie können z. B. die .NET-[StreamReader]-Klasse verwenden:

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

Sie können auch die ReadLines Methode von [System.IO.File]verwenden, die StreamReaderumschließt, vereinfacht den Lesevorgang:

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

Nachschlagen von Einträgen nach Eigenschaft in großen Sammlungen

Es ist üblich, dass Sie eine freigegebene Eigenschaft verwenden müssen, um denselben Datensatz in verschiedenen Sammlungen zu identifizieren, z. B. einen Namen zum Abrufen einer ID aus einer Liste und einer E-Mail aus einer anderen. Das Durchlaufen der ersten Liste, um den übereinstimmenden Datensatz in der zweiten Sammlung zu finden, ist langsam. Insbesondere hat die wiederholte Filterung der zweiten Sammlung einen großen Aufwand.

Bei zwei Auflistungen, eine mit einer -ID und Name, die andere mit Name und E-Mail-:

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

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

Die übliche Methode zum Abgleichen dieser Auflistungen zum Zurückgeben einer Liste von Objekten mit der -ID, Nameund Eigenschaften für E-Mail- könnte wie folgt aussehen:

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

Diese Implementierung muss jedoch alle 5000 Elemente in der $Accounts Auflistung einmal nach jedem Element in der $Employee-Auflistung filtern. Dies kann auch für diesen Einzelwert-Nachschlagevorgang Minuten dauern.

Stattdessen können Sie eine Hashtabelle erstellen, die die freigegebene Name Eigenschaft als Schlüssel und das übereinstimmende Konto als Wert verwendet.

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

Das Nachschlagen von Schlüsseln in einer Hashtabelle ist viel schneller als das Filtern einer Auflistung nach Eigenschaftswerten. Statt jedes Element in der Auflistung zu überprüfen, kann PowerShell überprüfen, ob der Schlüssel definiert ist und seinen Wert verwendet.

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

Dies ist viel schneller. Während der Schleifenfilter Minuten dauerte, dauert die Hashsuche weniger als eine Sekunde.

Write-Host sorgfältig verwenden

Der Befehl Write-Host sollte nur verwendet werden, wenn Sie formatierten Text in die Hostkonsole schreiben müssen, anstatt Objekte in die Success Pipeline zu schreiben.

Write-Host kann eine Größenordnung langsamer sein als [Console]::WriteLine() für bestimmte Hosts wie pwsh.exe, powershell.exeoder powershell_ise.exe. [Console]::WriteLine() funktioniert jedoch nicht garantiert in allen Hosts. Außerdem wird die Ausgabe, die mit [Console]::WriteLine() geschrieben wurde, nicht in Transkriptionen geschrieben, die von Start-Transcriptgestartet werden.

JIT-Kompilierung

PowerShell kompiliert den Skriptcode in Bytecode, der interpretiert wird. Ab PowerShell 3 kann PowerShell für Code, der wiederholt in einer Schleife ausgeführt wird, die Leistung verbessern, indem just-in-time (JIT) den Code in systemeigenem Code kompiliert.

Schleifen mit weniger als 300 Anweisungen sind für die JIT-Kompilierung geeignet. Schleifen sind größer als die zu kostspielige Kompilierung. Wenn die Schleife 16 Mal ausgeführt wurde, wird das Skript IM Hintergrund kompiliert. Nach Abschluss der JIT-Kompilierung wird die Ausführung an den kompilierten Code übertragen.

Vermeiden wiederholter Aufrufe einer Funktion

Das Aufrufen einer Funktion kann ein teurer Vorgang sein. Wenn Sie eine Funktion in einer lang ausgeführten engen Schleife aufrufen, sollten Sie die Schleife innerhalb der Funktion verschieben.

Betrachten Sie die folgenden Beispiele:

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

Das beispiel Basic for-loop ist die Basislinie für die Leistung. Im zweiten Beispiel wird der Zufallszahlengenerator in eine Funktion umbrochen, die in einer engen Schleife aufgerufen wird. Im dritten Beispiel wird die Schleife innerhalb der Funktion verschoben. Die Funktion wird nur einmal aufgerufen, aber der Code generiert immer noch die gleiche Menge an Zufallszahlen. Beachten Sie die Unterschiede bei den Ausführungszeiten für jedes Beispiel.

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

Vermeiden des Umbruchs von Cmdlet-Pipelines

Die meisten Cmdlets werden für die Pipeline implementiert, bei der es sich um eine sequenzielle Syntax und einen Prozess handelt. Zum Beispiel:

cmdlet1 | cmdlet2 | cmdlet3

Die Initialisierung einer neuen Pipeline kann teuer sein, daher sollten Sie vermeiden, eine Cmdlet-Pipeline in eine andere vorhandene Pipeline umzuschließen.

Betrachten Sie das folgende Beispiel. Die Input.csv Datei enthält 2100 Zeilen. Der Befehl Export-Csv wird in die ForEach-Object Pipeline eingeschlossen. Das cmdlet Export-Csv wird für jede Iteration der ForEach-Object Schleife aufgerufen.

$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

Im nächsten Beispiel wurde der Befehl Export-Csv außerhalb der ForEach-Object Pipeline verschoben. In diesem Fall wird Export-Csv nur einmal aufgerufen, verarbeitet aber weiterhin alle Objekte, die aus ForEach-Objectübergeben wurden.

$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

Das unwrappte Beispiel ist 372 Mal schneller. Beachten Sie außerdem, dass für die erste Implementierung der parameter "Append" erforderlich ist, der für die spätere Implementierung nicht erforderlich ist.

Objekterstellung

Das Erstellen von Objekten mithilfe des Cmdlets New-Object kann langsam sein. Der folgende Code vergleicht die Leistung des Erstellens von Objekten mithilfe des cmdlets New-Object mit der zugriffstaste [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 hat die new() statische Methode für alle .NET-Typen hinzugefügt. Der folgende Code vergleicht die Leistung des Erstellens von Objekten mithilfe des cmdlets New-Object mit der new()-Methode.

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

Verwenden von OrderedDictionary zum dynamischen Erstellen neuer Objekte

Es gibt Situationen, in denen objekte basierend auf einigen Eingaben dynamisch erstellt werden müssen, die vielleicht am häufigsten verwendete Methode zum Erstellen einer neuen PSObject- und dann neue Eigenschaften mithilfe des cmdlets Add-Member hinzufügen. Die Leistungskosten für kleine Sammlungen, die diese Technik verwenden, können jedoch für große Sammlungen sehr spürbar werden. In diesem Fall empfiehlt sich die Verwendung eines [OrderedDictionary] und anschließendes Konvertieren in ein PSObject- mithilfe der Zugriffstaste [pscustomobject] Typs. Weitere Informationen finden Sie im Abschnitt Erstellen von geordneten Wörterbüchern Abschnitt about_Hash_Tables.

Gehen Sie davon aus, dass die folgende API-Antwort in der Variablen $jsongespeichert ist.

{
  "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" ]
      ]
    }
  ]
}

Angenommen, Sie möchten diese Daten in eine CSV-Datei exportieren. Zuerst müssen Sie neue Objekte erstellen und die Eigenschaften und Werte mithilfe des Cmdlets Add-Member hinzufügen.

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

Mit einem OrderedDictionarykann der Code in Folgendes übersetzt werden:

$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 beiden Fällen wäre die $result Ausgabe gleich:

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

Der letztere Ansatz wird exponentiell effizienter, da die Anzahl der Objekte und Membereigenschaften steigt.

Hier ist ein Leistungsvergleich von drei Techniken zum Erstellen von Objekten mit 5 Eigenschaften:

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

Und dies sind die Ergebnisse:

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