Sdílet prostřednictvím


Aspekty výkonu skriptování PowerShellu

Skripty PowerShell, které využívají rozhraní .NET přímo a vyhýbají se pipeline, bývají rychlejší než idiomatický PowerShell. Idiomatic PowerShell používá cmdlety a funkce PowerShellu, často využívá kanál a k .NET se uchyluje pouze v případě potřeby.

Poznámka

Mnohé z technik popsaných zde nejsou idiomatické v PowerShellu a mohou snížit čitelnost PowerShellového skriptu. Autořům skriptů se doporučuje, aby používali idiomatický PowerShell, pokud tomu výkon nebrání.

Potlačení výstupu

Existuje mnoho způsobů, jak zabránit zápisu objektů do kanálu.

  • Přiřazení nebo přesměrování souboru na $null
  • Přetypování na [void]
  • Potrubí k Out-Null

Rychlosti přiřazování $null, přetypování na [void]a přesměrování souborů na $null jsou téměř stejné. Volání Out-Null ve velké smyčce ale může být výrazně pomalejší, zejména v PowerShellu 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'
        }
    }
}

Tyto testy byly spuštěny na počítači s Windows 11 v PowerShellu 7.3.4. Výsledky jsou uvedené níže:

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

Časy a relativní rychlosti se mohou lišit v závislosti na hardwaru, verzi PowerShellu a aktuální úloze v systému.

Přidání pole

Generování seznamu položek se často provádí pomocí pole s operátorem sčítání:

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

Poznámka

V PowerShellu 7.5 bylo přidání pole optimalizováno a už nevytváří pro každou operaci nové pole. Zde popsané aspekty výkonu se stále vztahují na verze PowerShellu starší než 7.5. Další informace najdete v tématu Co je nového v PowerShellu 7.5.

Přidání pole je neefektivní, protože pole mají pevnou velikost. Každé přidání do pole vytvoří nové pole dostatečně velké pro uložení všech prvků levého i pravého operandu. Prvky obou operandů se zkopírují do nového pole. U malých kolekcí nemusí být tento překryv důležitý. Výkon může být u velkých kolekcí snížen.

Existuje několik alternativ. Pokud pole ve skutečnosti nepotřebujete, zvažte použití zadaného obecného seznamu ([List<T>]):

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

Dopad použití přidávání prvků do pole na výkon se exponenciálně zvětšuje s velikostí kolekce a počtem přidání. Tento kód porovnává explicitně přiřazování hodnot k poli pomocí sčítání pole a použití metody Add(T) u objektu [List<T>]. Definuje explicitní přiřazení jako základ pro výkon.

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

Tyto testy byly spuštěny na počítači s Windows 11 v PowerShellu 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

Při práci s velkými kolekcemi je přidávání do pole výrazně pomalejší než přidávání do List<T>.

Při použití objektu [List<T>] je nutné vytvořit seznam s určitým typem, například [string] nebo [int]. Když do seznamu přidáte objekty jiného typu, přetypují se na zadaný typ. Pokud je nelze přetypovat na zadaný typ, metoda vyvolá výjimku.

$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

Pokud potřebujete, aby byl seznam kolekcí různých typů objektů, vytvořte ho s [Object] jako typ seznamu. Můžete sejmout seznam kolekce a zkontrolovat typy objektů v ní.

$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

Pokud potřebujete pole, můžete volat metodu ToArray() v seznamu nebo můžete nechat PowerShell vytvořit pole za vás:

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

V tomto příkladu PowerShell vytvoří [ArrayList] pro uložení výsledků zapsaných do kanálu uvnitř výrazu pole. Těsně před přiřazením k $resultsPowerShell převede [ArrayList] na [Object[]].

Sčítání řetězců

Řetězce jsou neměnné. Každý dodatek k řetězci ve skutečnosti vytvoří nový řetězec dostatečně velký, aby držel obsah levého i pravého operandu a potom zkopíruje prvky obou operandů do nového řetězce. U malých řetězců nemusí být tato režie důležitá. U velkých řetězců to může mít vliv na výkon a spotřebu paměti.

Existují alespoň dvě alternativy:

  • Operátor -join zřetězí řetězce
  • Třída [StringBuilder] .NET poskytuje proměnlivý řetězec.

Následující příklad porovnává výkon těchto tří metod sestavení řetězce.

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

Tyto testy byly spuštěny na počítači s Windows 11 v PowerShellu 7.4.2. Výstup ukazuje, že operátor -join je nejrychlejší, následovaný [StringBuilder] třídou.

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

Časy a relativní rychlosti se mohou lišit v závislosti na hardwaru, verzi PowerShellu a aktuální úloze v systému.

Zpracování velkých souborů

Idiotický způsob zpracování souboru v PowerShellu může vypadat nějak takto:

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

Může to být řádově pomalejší než přímé použití rozhraní .NET API. Můžete například použít třídu .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()
    }
}

Můžete také použít ReadLines metodu [System.IO.File], která obaluje StreamReader a zjednodušuje proces čtení.

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

Vyhledávání položek podle vlastností ve velkých kolekcích

K identifikaci stejného záznamu v různých kolekcích je běžné použít sdílenou vlastnost, například použití názvu k načtení ID z jednoho seznamu a e-mailu z jiného. Iterace přes první seznam k vyhledání odpovídajícího záznamu ve druhé kolekci je pomalá. Zejména opakované filtrování druhé kolekce představuje velkou režii.

Pokud máte dvě kolekce, jednu s ID a názvem, druhou s názvem a e-mailem:

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

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

Obvyklý způsob, jak tyto kolekce odsouhlasit, aby se vrátil seznam objektů s vlastnostmi ID, Název a E-mail , může vypadat takto:

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

Tato implementace však musí filtrovat všech 5000 položek v kolekci $Accounts jednou pro každou položku v kolekci $Employee. To může trvat minuty, dokonce i pro toto vyhledávání s jedinou hodnotou.

Místo toho můžete vytvořit tabulku hash, která jako klíč používá sdílenou vlastnost Název a odpovídající účet jako hodnotu.

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

Vyhledávání klíčů v tabulce hash je mnohem rychlejší než filtrování kolekce podle hodnot vlastností. Místo kontroly každé položky v kolekci může PowerShell zkontrolovat, jestli je klíč definovaný, a použít jeho hodnotu.

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

To je mnohem rychlejší. Zatímco dokončení cyklického filtru trvalo několik minut, vyhledávání pomocí hash tabulky trvá méně než sekundu.

Pečlivě používejte Write-Host

Příkaz Write-Host by se měl použít jenom v případě, že potřebujete napsat formátovaný text do hostitelské konzoly, a ne psát objekty do kanálu Success.

Write-Host může být řádově pomalejší než [Console]::WriteLine() pro konkrétní hostitele, jako jsou pwsh.exe, powershell.exenebo powershell_ise.exe. Není však zaručeno, že [Console]::WriteLine() bude fungovat ve všech hostitelích. Výstup napsaný pomocí [Console]::WriteLine() se také nezapisuje do přepisů zahájených Start-Transcript.

kompilace JIT

PowerShell zkompiluje kód skriptu do bajtového kódu, který se interpretuje. Od verze PowerShell 3 může PowerShell pro kód, který se opakovaně spouští ve smyčce, zlepšit výkon tím, že kód kompiluje jako nativní kód pomocí Just-in-time (JIT) kompilace.

Smyčky, které mají méně než 300 instrukcí, mají nárok na kompilaci JIT. Smyčky větší než toto jsou příliš nákladné na kompilaci. Když se smyčka spustí 16krát, skript se zkompiluje na pozadí. Po dokončení kompilace JIT se provádění přenese do zkompilovaného kódu.

Vyhněte se opakovaným voláním funkce

Volání funkce může být náročná operace. Pokud voláte funkci v dlouhotrvající těsné smyčce, zvažte přesunutí smyčky uvnitř funkce.

Podívejte se na následující příklady:

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

Příkladem Basic for-loop je základní čára výkonu. Druhý příklad ukazuje generátor náhodných čísel zabalený do funkce, která je volána v úzké smyčce. Třetí příklad přesune smyčku dovnitř funkce. Funkce se volá pouze jednou, ale kód stále generuje stejné množství náhodných čísel. Všimněte si rozdílu v časech provádění pro každý příklad.

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

Vyhněte se obtékání kanálů cmdletů

Většina cmdletů je implementována pro datový proud, což je sekvenční syntaxe a proces. Například:

cmdlet1 | cmdlet2 | cmdlet3

Inicializace nového kanálu může být nákladná, proto byste se měli vyhnout zabalení kanálu příkazů cmdlet do jiného, již existujícího kanálu.

Podívejte se na následující příklad. Soubor Input.csv obsahuje 2100 řádků. Příkaz Export-Csv je zabalený uvnitř kanálu ForEach-Object. Rutina cmdlet Export-Csv se vyvolá pro každou iteraci smyčky 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

V dalším příkladu se příkaz Export-Csv přesunul mimo kanál ForEach-Object. V tomto případě se Export-Csv vyvolá pouze jednou, ale stále zpracovává všechny objekty předané 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

Příklad rozbalení je 372-krát rychlejší. Všimněte si také, že první implementace vyžaduje parametr Append, který není vyžadován pro pozdější implementaci.

Vyhněte se nepotřebným výčtům kolekcí

Operátory porovnání PowerShellu mají při porovnávání kolekcí funkci konvience. Pokud je hodnota vlevo ve výrazu kolekce, vrátí operátor prvky kolekce, které odpovídají pravé hodnotě výrazu.

Tato funkce poskytuje jednoduchý způsob filtrování kolekce. Například:

PS> $Collection = 1..99
PS> ($Collection -like '*1*') -join ' '

1 10 11 12 13 14 15 16 17 18 19 21 31 41 51 61 71 81 91

Pokud však použijete porovnání kolekce v podmíněném příkazu, který očekává pouze logický výsledek, může tato funkce vést k nízkému výkonu.

Příklad:

if ($Collection -like '*1*') { 'Found' }

V tomto příkladu PowerShell porovná pravou hodnotu s každou hodnotou v kolekci a vrátí kolekci výsledků. Vzhledem k tomu, že výsledek není prázdný, výsledek, který není null, se vyhodnotí jako $true. Podmínka platí, když se najde první shoda, ale PowerShell stále vytvoří výčet celé kolekce. Tento výčet může mít významný dopad na výkon u velkých kolekcí.

Jedním ze způsobů, jak zvýšit výkon, je použít Where() metodu kolekce. Metoda Where() přestane vyhodnocovat kolekci, jakmile najde první shodu.

# Create an array of 1048576 items
$Collection = foreach ($x in 1..1MB) { $x }
(Measure-Command { if ($Collection -like '*1*') { 'Found' } }).TotalMilliseconds
633.3695
(Measure-Command { if ($Collection.Where({ $_ -like '*1*' }, 'first')) { 'Found' } }).TotalMilliseconds
2.607

U milionů položek je použití Where() metody výrazně rychlejší.

Vytvoření objektu

Vytváření objektů pomocí rutiny New-Object může být pomalé. Následující kód porovnává výkon vytváření objektů pomocí rutiny New-Object s akcelerátorem typů [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 přidal new() statickou metodu pro všechny typy .NET. Následující kód porovnává výkon vytváření objektů pomocí rutiny New-Object s new() metodou.

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

Použití OrderedDictionary k dynamickému vytváření nových objektů

Existují situace, kdy možná budeme muset dynamicky vytvářet objekty na základě určitého vstupu, možná nejčastěji používaný způsob, jak vytvořit nový PSObject a pak přidat nové vlastnosti pomocí rutiny Add-Member. Náklady na výkon malých kolekcí používající tuto techniku mohou být zanedbatelné, ale u velkých kolekcí se můžou velmi znatelné. V takovém případě doporučujeme použít [OrderedDictionary] a pak ho převést na PSObject pomocí akcelerátoru typu [pscustomobject]. Další informace najdete v části Vytváření uspořádaných slovníkůabout_Hash_Tables.

Předpokládejme, že máte uloženou následující odpověď rozhraní API v proměnné $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" ]
      ]
    }
  ]
}

Předpokládejme, že teď chcete tato data exportovat do CSV. Nejprve musíte vytvořit nové objekty a přidat vlastnosti a hodnoty pomocí rutiny 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
}

Pomocí OrderedDictionarylze kód přeložit 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
}

V obou případech by byl výstup $result stejný:

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

Druhý přístup se stává exponenciálně efektivnějším s rostoucím počtem objektů a vlastností členů.

Tady je porovnání výkonu tří technik pro vytváření objektů s 5 vlastnostmi:

$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 to jsou výsledky:

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