Überlegungen zur Leistung bei der Skripterstellung in PowerShell
PowerShell-Skripts, die .NET direkt nutzen und die Pipeline vermeiden, sind in der Regel schneller als die idiomatische PowerShell-Methode. Bei der idiomatischen PowerShell-Methode werden Cmdlets und PowerShell-Funktionen verwendet, und es wird häufig die Pipeline genutzt. Auf .NET wird nur bei Bedarf zurückgegriffen.
Hinweis
Viele der hier beschriebenen Methoden sind keine idiomatischen PowerShell-Methoden und können die Lesbarkeit eines PowerShell-Skripts beeinträchtigen. Skriptautoren wird empfohlen, die idiomatische PowerShell-Methode zu verwenden, sofern aus Leistungsgründen nicht anders vorgegeben.
Unterdrücken der Ausgabe
Es gibt viele Möglichkeiten, das Schreiben von Objekten in die Pipeline zu vermeiden.
- Zuweisung oder Dateiumleitung an
$null
- Umwandlung in
[void]
- Pipe an
Out-Null
Bei der Zuweisung an $null
, der Umwandlung in [void]
und der Dateiumleitung an $null
sind die Geschwindigkeiten nahezu identisch. Der Aufruf von Out-Null
in einer großen Schleife kann jedoch deutlich 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. Hier sehen Sie die Ergebnisse:
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, PowerShell-Version und aktueller Workload des Systems variieren.
Addition eines Arrays
Das Generieren einer Liste von Elementen erfolgt häufig mithilfe eines Arrays mit dem Additionsoperator:
$results = @()
$results += Get-Something
$results += Get-SomethingElse
$results
Die Addition eines Arrays ist ineffizient, da Arrays eine feste Größe haben. Bei jeder Ergänzung des Arrays wird jeweils ein neues Array erstellt, das groß genug ist, um alle Elemente des linken und rechten Operanden zu enthalten. Die Elemente beider Operanden werden in das neue Array kopiert. Bei kleinen Sammlungen kann dieser Mehraufwand unerheblich sein. Bei großen Sammlungen kommt es möglicherweise zu einer Beeinträchtigung der Leistung.
Es gibt eine Reihe von Alternativen. Wenn Sie kein Array benötigen, sollten Sie stattdessen eine typisierte generische Liste ([List<T>]
) verwenden:
$results = [System.Collections.Generic.List[object]]::new()
$results.AddRange((Get-Something))
$results.AddRange((Get-SomethingElse))
$results
Die Leistungsauswirkung bei Addition eines Arrays wächst exponentiell mit der Größe der Sammlung und den Zahlenadditionen. Dieser Code vergleicht das explizite Zuweisen von Werten zu einem Array mit der Anwendung der Arrayaddition und der Anwendung der Add(T)
-Methode auf ein [List<T>]
-Objekt. Er definiert die explizite Zuweisung als Baseline 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 Auflistungen arbeiten, ist die Arrayaddition erheblich langsamer als das Hinzufügen zu einem List<T>
-Objekt.
Wenn Sie ein [List<T>]
-Objekt verwenden, müssen Sie die Liste mit einem bestimmten Typ erstellen, z. B. mit [String]
oder [Int]
. Wenn Sie der Liste Objekte eines anderen Typs hinzufügen, werden sie in den angegebenen Typ umgewandelt. Wenn sie nicht in den angegebenen Typ umgewandelt werden 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 Sie die Liste als Auflistung verschiedener Objekttypen benötigen, erstellen Sie sie mit [Object]
als Listentyp. Sie können die Sammlung auflisten, um die Typen der in ihr enthaltenen Objekte zu untersuchen.
$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 Methode ToArray()
auf der Liste aufrufen oder PowerShell das Array für Sie erstellen lassen:
$results = @(
Get-Something
Get-SomethingElse
)
In diesem Beispiel erstellt PowerShell ein [ArrayList]
-Element, um die im Arrayausdruck in die Pipeline geschriebenen Ergebnisse zu speichern. Kurz vor dem Zuweisen zu $results
konvertiert PowerShell das [ArrayList]
-Element in ein [Object[]]
-Element.
Addition von Zeichenfolgen
Zeichenfolgen sind unveränderlich. Durch jede Addition zur Zeichenfolge wird in Wirklichkeit eine neue Zeichenfolge erstellt, die groß genug ist, um alle Elemente des linken und rechten Operanden aufzunehmen. Anschließend werden die Elemente beider Operanden in die neue Zeichenfolge kopiert. Bei kleinen Zeichenfolgen spielt dieser Mehraufwand möglicherweise keine Rolle. Bei großen Zeichenfolgen kann sich das auf die Leistung und dir Arbeitsspeicherauslastung auswirken.
Es gibt mindestens zwei Alternativen:
- Der
-join
-Operator verkettet Zeichenfolgen. - Die .NET-Klasse
[StringBuilder]
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, danach folgt die [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, PowerShell-Version und aktueller Workload des Systems variieren.
Verarbeiten großer Dateien
Die idiomatische Methode zum Verarbeiten einer Datei in PowerShell könnte in etwa wie folgt aussehen:
Get-Content $path | Where-Object Length -GT 10
Dies kann erheblich langsamer sein als die direkte Verwendung von .NET-APIs. Sie können z. B. die .NET-Klasse [StreamReader]
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 StreamReader
umschließt und den Lesevorgang vereinfacht:
foreach ($line in [System.IO.File]::ReadLines($path)) {
if ($line.Length -gt 10) {
$line
}
}
Suchen nach Einträgen nach Eigenschaft in großen Sammlungen
Es ist üblich, dass Sie eine freigegebene Eigenschaft verwenden müssen, um den gleichen Datensatz in verschiedenen Sammlungen zu identifizieren, z. B. mithilfe eines Namens zum Abrufen einer ID aus einer Liste und einer E-Mail aus einer anderen. Es dauert lange, die erste Liste zu durchlaufen, um den übereinstimmenden Datensatz in der zweiten Sammlung zu finden. Insbesondere die wiederholte Filterung der zweiten Sammlung bedeutet einen großen Mehraufwand.
Nehmen wir an, es gibt zwei Sammlungen, eine mit einer ID und einen Namen, die andere mit einem Namen und einer 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 Sammlungen, um eine Liste von Objekten mit den Eigenschaften ID, Name und Email zurückzugeben, 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 Sammlung $Accounts
einmal für jedes Element in der Sammlung $Employee
filtern. Dies kann bei der Einzelwertsuche einige Minuten dauern.
Sie können stattdessen 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 Suchen nach Schlüsseln in einer Hashtabelle ist viel schneller als das Filtern einer Sammlung nach Eigenschaftswerten. Anstatt jedes Element in der Sammlung überprüfen zu müssen, kann PowerShell überprüfen, ob der Schlüssel definiert ist, und seinen Wert verwenden.
$Results = $Employees | ForEach-Object -Process {
$Email = $LookupHash[$_.Name].Email
[pscustomobject]@{
Id = $_.Id
Name = $_.Name
Email = $Email
}
}
Das geht viel schneller. Während der Schleifenfilter mehrere Minuten gebraucht hat, dauert die Hashsuche weniger als eine Sekunde.
Sorgfältiges Verwenden von Write-Host
Der Befehl Write-Host
sollte nur verwendet werden, wenn Sie formatierten Text an der Hostkonsole schreiben müssen, anstatt Objekte in die Success-Pipeline zu schreiben.
Write-Host
kann für bestimmte Hosts wie pwsh.exe
, powershell.exe
oder powershell_ise.exe
erheblich langsamer sein als [Console]::WriteLine()
. [Console]::WriteLine()
funktioniert jedoch möglicherweise nicht auf allen Hosts. Außerdem werden mit [Console]::WriteLine()
geschriebene Ausgaben nicht in Transkripte geschrieben, die von Start-Transcript
gestartet 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 der Code per JIT-Verfahren (Just-In-Time) in nativen Code kompiliert wird.
Schleifen mit weniger als 300 Anweisungen sind für die JIT-Kompilierung geeignet. Bei größeren Schleifen ist diese Kompilierung zu kostspielig. Wenn die Schleife 16-mal ausgeführt wurde, wird das Skript im Hintergrund per JIT kompiliert. Wenn die JIT-Kompilierung abgeschlossen ist, wird die Ausführung an den kompilierten Code übergeben.
Vermeiden wiederholter Aufrufe einer Funktion
Das Aufrufen einer Funktion kann ein kostspieliger Vorgang sein. Wenn Sie eine Funktion in einer zeitintensiven, engen Schleife aufrufen, sollten Sie erwägen, die Schleife in die Funktion zu 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 Baseline für die Leistung. Das zweite Beispiel umschließt den Zufallszahlengenerator in einer Funktion, die in einer engen Schleife aufgerufen wird. Im dritten Beispiel wird die Schleife in die Funktion verschoben. Die Funktion wird nur einmal aufgerufen, aber der Code generiert trotzdem dieselbe Anzahl Zufallszahlen. Beachten Sie den Unterschied 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 Umschließens von Cmdlet-Pipelines
Die meisten Cmdlets werden für die Pipeline implementiert. Hierbei handelt es sich um eine sequenzielle Syntax und einen entsprechenden Prozess. Zum Beispiel:
cmdlet1 | cmdlet2 | cmdlet3
Das Initialisieren einer neuen Pipeline kann aufwendig sein. Vermeiden Sie es daher, eine Cmdlet-Pipeline in eine andere vorhandene Pipeline einzuschließen.
Sehen Sie sich das folgende Beispiel an. Die Datei Input.csv
enthält 2.100 Zeilen. Der Befehl Export-Csv
ist in die Pipeline ForEach-Object
eingeschlossen. Das Cmdlet Export-Csv
wird für jede Iteration der Schleife vom Typ ForEach-Object
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 Pipeline ForEach-Object
platziert.
In diesem Fall wird Export-Csv
nur einmal aufgerufen. Es werden aber trotzdem alle Objekte verarbeitet, die von ForEach-Object
übergeben werden.
$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 Beispiel ohne Umschließung ist 372-mal schneller. Beachten Sie außerdem, dass die erste Implementierung den Parameter Append erfordert. In der späteren Implementierung wird er dagegen nicht benötigt.
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 dem [pscustomobject]
-Typbeschleunigers.
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. Dies ist die vielleicht am häufigsten verwendete Methode zum Erstellen eines neuen PSObjects. Anschließend können sie neue Eigenschaften mithilfe des CmdletsAdd-Member
hinzufügen. Die Leistungseinbußen bei kleinen Sammlungen können bei dieser Technik vernachlässigbar sein, bei großen Sammlungen können sie jedoch deutlich spürbar werden. In diesem Fall empfiehlt sich die Verwendung eines [OrderedDictionary]
und anschließenden Konvertieren in ein PSObject mithilfe des Typbeschleunigers [pscustomobject]
. Weitere Informationen finden Sie im Abschnitt Erstellen sortierter Wörterbücher von about_Hash_Tables.
Gehen Sie davon aus, dass die folgende API-Antwort in der Variablen $json
gespeichert 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 nun als 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
}
Mithilfe eines OrderedDictionary
kann 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 Ausgabe von $result
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