Udostępnij za pośrednictwem


Zagadnienia dotyczące wydajności skryptów programu PowerShell

Skrypty programu PowerShell, które korzystają bezpośrednio z platformy .NET i unikają potoku, wydają się być szybsze niż idiomatyczny program PowerShell. Idiomatyczny program PowerShell używa poleceń cmdlet i funkcji programu PowerShell, często korzystających z potoku i uciekających się do platformy .NET tylko wtedy, gdy jest to konieczne.

Notatka

Wiele technik opisanych w tym miejscu nie jest idiomatycznych programu PowerShell i może zmniejszyć czytelność skryptu programu PowerShell. Autorzy skryptów powinni używać idiomatycznego PowerShell, chyba że wydajność dyktuje inaczej.

Tłumienie danych wyjściowych

Istnieje wiele sposobów na uniknięcie zapisywania obiektów w potoku.

  • Przypisanie lub przekierowanie pliku do $null
  • Rzutowanie do [void]
  • Potok do Out-Null

Szybkość przypisywania do $null, rzutowania do [void]i przekierowywania plików do $null jest prawie identyczna. Jednak wywoływanie Out-Null w dużej pętli może być znacznie wolniejsze, zwłaszcza w programie 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'
        }
    }
}

Te testy zostały uruchomione na maszynie z systemem Windows 11 w programie PowerShell 7.3.4. Poniżej przedstawiono wyniki:

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

Czasy i względne szybkości mogą się różnić w zależności od sprzętu, wersji programu PowerShell i bieżącego obciążenia w systemie.

Dodawanie tablic

Generowanie listy elementów jest często wykonywane przy użyciu tablicy z operatorem dodawania:

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

Notatka

W programie PowerShell 7.5 dodanie tablicy zostało zoptymalizowane i nie tworzy już nowej tablicy dla każdej operacji. Zagadnienia dotyczące wydajności opisane tutaj nadal dotyczą wersji programu PowerShell wcześniejszych niż 7.5. Aby uzyskać więcej informacji, zobacz Co nowego w programie PowerShell 7.5.

Dodawanie tablic może być nieefektywne, ponieważ tablice mają stały rozmiar. Każde dodanie do tablicy tworzy nową tablicę, wystarczająco dużą, aby pomieścić wszystkie elementy zarówno z lewego, jak i prawego operandu. Elementy każdego z dwóch operandów są kopiowane do nowej tablicy. W przypadku małych kolekcji to obciążenie może nie mieć znaczenia. Wydajność może ucierpieć w przypadku dużych kolekcji.

Istnieje kilka alternatyw. Jeśli w rzeczywistości nie potrzebujesz tablicy, rozważ użycie wpisanej listy ogólnej ([List<T>]):

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

Wpływ na wydajność użycia dodawania do tablic rośnie wykładniczo wraz z rozmiarem kolekcji i liczbą dodanych elementów. Ten kod porównuje jawne przypisywanie wartości do tablicy z użyciem operacji dodawania tablic oraz użycie metody Add(T) w obiekcie [List<T>]. Definiuje jawne przypisanie jako podstawę dla wydajności.

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

Te testy zostały uruchomione na maszynie z systemem Windows 11 w programie 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

Podczas pracy z dużymi kolekcjami dodawanie tablic jest znacznie wolniejsze niż dodawanie do List<T>.

W przypadku korzystania z obiektu [List<T>] należy utworzyć listę z określonym typem, na przykład [string] lub [int]. Po dodaniu obiektów innego typu do listy są one rzutowane do określonego typu. Jeśli nie można ich rzutować na określony typ, metoda zgłasza wyjątek.

$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

Gdy lista musi być kolekcją różnych typów obiektów, utwórz ją przy użyciu [Object] jako typu listy. Możesz wyliczyć kolekcję i sprawdzić typy obiektów w niej.

$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

Jeśli potrzebujesz tablicy, możesz wywołać metodę ToArray() na liście lub umożliwić programowi PowerShell utworzenie tablicy dla Ciebie:

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

W tym przykładzie program PowerShell tworzy [ArrayList] do przechowywania wyników zapisanych w potoku wewnątrz wyrażenia tablicy. Tuż przed przypisaniem do $resultsprogram PowerShell konwertuje [ArrayList] na [Object[]].

Dodawanie ciągu

Ciągi są niezmienne. Każdy dodatek do ciągu faktycznie tworzy nowy ciąg wystarczająco duży, aby przechowywać zawartość zarówno lewych, jak i prawych operandów, a następnie kopiuje elementy obu operandów do nowego ciągu. W przypadku małych ciągów to obciążenie może nie mieć znaczenia. W przypadku dużych ciągów może to mieć wpływ na wydajność i zużycie pamięci.

Istnieją co najmniej dwie alternatywy:

  • Operator -join łączy ciągi
  • Klasa .NET [StringBuilder] udostępnia modyfikowalny ciąg

Poniższy przykład porównuje wydajność tych trzech metod tworzenia ciągu.

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

Te testy zostały uruchomione na maszynie z systemem Windows 11 w programie PowerShell 7.4.2. Wyniki pokazują, że operator -join jest najszybszy, a następnie klasa [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

Czasy i względne szybkości mogą się różnić w zależności od sprzętu, wersji programu PowerShell i bieżącego obciążenia w systemie.

Przetwarzanie dużych plików

Idiomatyczny sposób przetwarzania pliku w programie PowerShell może wyglądać następująco:

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

To może być o rząd wielkości wolniejsze niż bezpośrednie korzystanie z interfejsów API platformy .NET. Na przykład możesz użyć klasy .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()
    }
}

Można również użyć metody ReadLines[System.IO.File], która opakowuje StreamReader, upraszcza proces odczytywania:

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

Szukanie wpisów według właściwości w dużych kolekcjach

Często należy używać właściwości udostępnionej do identyfikowania tego samego rekordu w różnych kolekcjach, na przykład przy użyciu nazwy w celu pobrania identyfikatora z jednej listy i wiadomości e-mail z innej. Iterowanie na pierwszej liście w celu znalezienia pasującego rekordu w drugiej kolekcji jest powolne. W szczególności powtarzające się filtrowanie drugiej kolekcji ma duże obciążenie.

Dla dwóch kolekcji, jednej z identyfikatorem i nazwą , drugiej z nazwą i e-mailem :

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

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

Zwykły sposób uzgadniania tych kolekcji, aby zwrócić listę obiektów z właściwościami: identyfikator , nazwa oraz e-mail , może wyglądać następująco:

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

Jednak ta implementacja musi filtrować wszystkie 5000 elementów w kolekcji $Accounts raz dla każdego elementu w kolekcji $Employee. Może to potrwać kilka minut, nawet w przypadku tego wyszukiwania pojedynczej wartości.

Zamiast tego możesz utworzyć Tablicę skrótów, która używa udostępnionej właściwości Name jako klucza i pasującego konta jako wartości.

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

Szukanie kluczy w tabeli skrótów jest znacznie szybsze niż filtrowanie kolekcji według wartości właściwości. Zamiast sprawdzać każdy element w kolekcji, program PowerShell może sprawdzić, czy klucz jest zdefiniowany i używać jego wartości.

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

Jest to znacznie szybsze. Chociaż działanie filtra pętli zajęło kilka minut, wyszukiwanie skrótu trwa mniej niż sekundę.

Ostrożnie używaj Write-Host

Polecenia Write-Host należy używać tylko wtedy, gdy trzeba zapisać sformatowany tekst w konsoli hosta, zamiast zapisywać obiekty w potoku Success.

Write-Host może być o rząd wielkości wolniejsze niż [Console]::WriteLine() dla określonych hostów, takich jak pwsh.exe, powershell.exe, lub powershell_ise.exe. Jednak [Console]::WriteLine() nie ma gwarancji, że działa we wszystkich hostach. Ponadto dane wyjściowe zapisywane przy użyciu [Console]::WriteLine() nie są zapisywane w transkrypcjach rozpoczętych przez Start-Transcript.

kompilacja JIT

Program PowerShell kompiluje kod skryptu do kodu bajtowego, który jest interpretowany. Począwszy od programu PowerShell 3, w przypadku kodu, który jest wielokrotnie wykonywany w pętli, program PowerShell może zwiększyć wydajność dzięki kompilowaniu kodu just in time (JIT) do kodu natywnego.

Pętle z mniej niż 300 instrukcjami kwalifikują się do kompilacji JIT. Pętle większe od tych są zbyt kosztowne do skompilowania. Po uruchomieniu pętli 16 razy, skrypt jest kompilowany metodą JIT w tle. Po zakończeniu kompilacji JIT wykonywanie jest przenoszone do skompilowanego kodu.

Unikaj powtarzających się wywołań funkcji

Wywoływanie funkcji może być kosztowną operacją. Jeśli wywołujesz funkcję w długo działającej wąskiej pętli, rozważ umieszczenie pętli wewnątrz funkcji.

Rozważmy następujące przykłady:

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

Przykład podstawowej pętli for stanowi punkt odniesienia dla wydajności. Drugi przykład opakowuje generator liczb losowych w funkcji, która jest wywoływana w ciasnej pętli. Trzeci przykład przenosi pętlę wewnątrz funkcji. Funkcja jest wywoływana tylko raz, ale kod nadal generuje tę samą liczbę losowych liczb. Zwróć uwagę na różnicę czasu wykonywania dla każdego przykładu.

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

Unikaj zawijania potoków cmdletów

Większość poleceń cmdlet jest implementowanych dla potoku, co jest sekwencyjną składnią i procesem. Na przykład:

cmdlet1 | cmdlet2 | cmdlet3

Inicjowanie nowego potoku może być kosztowne, dlatego należy unikać łączenia potoku cmdlet z innym istniejącym potokiem.

Rozważmy poniższy przykład. Plik Input.csv zawiera 2100 wierszy. Polecenie Export-Csv jest zawarte w potoku ForEach-Object. Polecenie cmdlet Export-Csv jest wywoływane dla każdej iteracji pętli 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

W następnym przykładzie polecenie Export-Csv zostało przeniesione poza potok ForEach-Object. W takim przypadku Export-Csv jest wywoływana tylko raz, ale nadal przetwarza wszystkie obiekty z 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

Rozpakowany przykład jest 372 razy szybszy. Należy również zauważyć, że pierwsza implementacja wymaga parametru Append, który nie jest wymagany do późniejszej implementacji.

Tworzenie obiektu

Tworzenie obiektów przy użyciu polecenia cmdlet New-Object może być powolne. Poniższy kod porównuje wydajność tworzenia obiektów przy użyciu polecenia cmdlet New-Object do akceleratora typu [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

Program PowerShell 5.0 dodał metodę statyczną new() dla wszystkich typów platformy .NET. Poniższy kod porównuje wydajność tworzenia obiektów przy użyciu polecenia cmdlet New-Object do metody 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

Używanie elementu OrderedDictionary do dynamicznego tworzenia nowych obiektów

Istnieją sytuacje, w których może być konieczne dynamiczne tworzenie obiektów na podstawie niektórych danych wejściowych, być może najczęściej używanym sposobem utworzenia nowego PSObject, a następnie dodania nowych właściwości przy użyciu polecenia cmdlet Add-Member. Koszt wydajności małych kolekcji korzystających z tej techniki może być niewielki, jednak może stać się bardzo zauważalny w przypadku dużych kolekcji. W takim przypadku zalecaną metodą jest użycie [OrderedDictionary], a następnie przekonwertowanie go na PSObject przy użyciu akceleratora typu [pscustomobject]. Aby uzyskać więcej informacji, zobacz sekcję Tworzenie uporządkowanych słownikówabout_Hash_Tables.

Załóżmy, że masz następującą odpowiedź interfejsu API przechowywaną w zmiennej $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" ]
      ]
    }
  ]
}

Teraz załóżmy, że chcesz wyeksportować te dane do pliku CSV. Najpierw należy utworzyć nowe obiekty i dodać właściwości i wartości przy użyciu polecenia 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
}

Przy użyciu OrderedDictionarykod można przetłumaczyć na:

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

W obu przypadkach dane wyjściowe $result byłyby takie same:

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

Drugie podejście staje się bardziej wydajne wykładniczo, ponieważ liczba obiektów i właściwości składowych wzrasta.

Poniżej przedstawiono porównanie wydajności trzech technik tworzenia obiektów z 5 właściwościami:

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

A oto wyniki:

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