PowerShell betik performansıyla ilgili dikkat edilmesi gerekenler

.NET'ten doğrudan yararlanan ve işlem hattından kaçınan PowerShell betikleri, idiomatic PowerShell'den daha hızlı olma eğilimindedir. Idiomatic PowerShell, genellikle işlem hattından yararlanan ve yalnızca gerektiğinde .NET'e başvuran cmdlet'leri ve PowerShell işlevlerini kullanır.

Not

Burada açıklanan tekniklerin çoğu idiomatic PowerShell değildir ve PowerShell betiğinin okunabilirliğini azaltabilir. Performans aksini gerektirmedikçe betik yazarlarının idiomatic PowerShell kullanması önerilir.

Çıktıyı gizleme

İşlem hattına nesne yazmaktan kaçınmanın birçok yolu vardır.

  • Atama veya dosya yeniden yönlendirme $null
  • Atama [void]
  • Boru hattı Out-Null

öğesine atama $null, öğesine atama, öğesine atama [void]ve dosya yeniden yönlendirme $null hızları neredeyse aynıdır. Ancak, özellikle PowerShell 5.1'de büyük bir döngüde çağrı Out-Null yapılması önemli ölçüde daha yavaş olabilir.

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

Bu testler PowerShell 7.3.4'teki bir Windows 11 makinesinde çalıştırıldı. Sonuçlar aşağıda gösterilmiştir:

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

Süreler ve göreli hızlar donanıma, PowerShell sürümüne ve sistemdeki geçerli iş yüküne bağlı olarak değişebilir.

Dizi ekleme

Öğe listesi oluşturma işlemi genellikle ekleme işlecine sahip bir dizi kullanılarak yapılır:

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

Diziler sabit bir boyuta sahip olduğundan dizi ekleme verimsizdir. Diziye her ekleme, hem sol hem de sağ işlenenlerin tüm öğelerini barındıracak kadar büyük yeni bir dizi oluşturur. her iki işlenenin öğeleri yeni diziye kopyalanır. Küçük koleksiyonlar için bu ek yük önemli olmayabilir. Büyük koleksiyonlarda performans olumsuz etkilenebilir.

Birkaç alternatif vardır. Bir diziye gerçekten ihtiyacınız yoksa, bunun yerine yazılan genel bir liste ([List<T>]):

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

Dizi ekleme kullanmanın performans etkisi, koleksiyonun boyutu ve sayı eklemeleri ile üstel olarak artar. Bu kod, bir diziye açıkça değer atama işlemini dizi ekleme ve bir nesne üzerinde Add(T)[List<T>] yöntemini kullanma ile karşılaştırır. Açık atamayı performans için temel olarak tanımlar.

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

Bu testler PowerShell 7.3.4'teki bir Windows 11 makinesinde çalıştırıldı.

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

Büyük koleksiyonlarla çalışırken, dizi ekleme işlemine eklemekten çok daha yavaştır List<T>.

Nesne [List<T>] kullanırken, listeyi veya [Int]gibi [String] belirli bir türle oluşturmanız gerekir. Listeye farklı türde nesneler eklediğinizde, bunlar belirtilen türe atılır. Belirtilen türe geçirilemiyorsa, yöntemi bir özel durum oluşturur.

$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

Listenin farklı nesne türlerinden oluşan bir koleksiyon olması gerektiğinde, liste türü olarak ile [Object] oluşturun. Koleksiyonu numaralandırarak içindeki nesnelerin türlerini inceleyebilirsiniz.

$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

Diziye ihtiyacınız varsa, listede yöntemini ToArray() çağırabilir veya PowerShell'in sizin için dizi oluşturmasına izin vekleyebilirsiniz:

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

Bu örnekte PowerShell, dizi ifadesinin içinde işlem hattına yazılan sonuçları tutmak için bir [ArrayList] oluşturur. öğesine atamadan $resultshemen önce PowerShell, öğesini [ArrayList] öğesine [Object[]]dönüştürür.

Dize ekleme

Dizeler sabittir. Dizeye yapılan her ekleme, hem sol hem de sağ işlenenlerin içeriğini tutacak kadar büyük yeni bir dize oluşturur ve her iki işlenenin öğelerini de yeni dizeye kopyalar. Küçük dizeler için bu ek yük önemli olmayabilir. Büyük dizeler için bu, performansı ve bellek tüketimini etkileyebilir.

En az iki alternatif vardır:

  • -join işleci dizeleri birleştirir
  • .NET [StringBuilder] sınıfı, değiştirilebilir bir dize sağlar

Aşağıdaki örnek, bu üç dize oluşturma yönteminin performansını karşılaştırır.

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

Bu testler PowerShell 7.4.2'deki bir Windows 11 makinesinde çalıştırıldı. Çıkış, işlecin en hızlı ve ardından sınıfı olduğunu [StringBuilder] gösterir-join.

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

Süreler ve göreli hızlar donanıma, PowerShell sürümüne ve sistemdeki geçerli iş yüküne bağlı olarak değişebilir.

Büyük dosyaları işleme

PowerShell'de bir dosyayı işlemek için idiomatic yöntemi şuna benzer olabilir:

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

Bu, .NET API'lerini doğrudan kullanmaktan daha yavaş bir büyüklük sırası olabilir. Örneğin, .NET [StreamReader] sınıfını kullanabilirsiniz:

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

öğesini sarmalayan ReadLinesStreamReaderve okuma işlemini basitleştiren yöntemini [System.IO.File]de kullanabilirsiniz:

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

Büyük koleksiyonlarda özelliğe göre girdileri arama

Bir listeden kimlik ve başka bir listeden e-posta almak için bir ad kullanmak gibi farklı koleksiyonlardaki aynı kaydı tanımlamak için paylaşılan bir özelliğin kullanılması yaygın bir durumdur. İkinci koleksiyonda eşleşen kaydı bulmak için ilk listede yineleme yavaştır. Özellikle, ikinci koleksiyonun yinelenen filtrelemesinin büyük bir yükü vardır.

Biri Kimliği ve Adı, diğeri Ad ve E-posta içeren iki koleksiyon verilip verilse:

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

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

Kimlik, Ad ve E-posta özelliklerine sahip nesnelerin listesini döndürmek için bu koleksiyonları uzlaştırmanın olağan yolu şöyle görünebilir:

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

Ancak, bu uygulamanın koleksiyondaki her öğe için koleksiyondaki $Accounts 5000 öğenin tümünü bir kez filtrelemesi $Employee gerekir. Bu tek değerli arama için bile dakikalar sürebilir.

Bunun yerine, anahtar olarak paylaşılan Ad özelliğini ve değer olarak eşleşen hesabı kullanan bir Karma Tablo oluşturabilirsiniz.

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

Karma tablodaki anahtarları aramak, bir koleksiyonu özellik değerlerine göre filtrelemekten çok daha hızlıdır. PowerShell, koleksiyondaki her öğeyi denetlemek yerine anahtarın tanımlanıp tanımlanmadığını denetleyebilir ve değerini kullanabilir.

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

Bu çok daha hızlı. Döngü filtresinin tamamlanması dakikalar sürerken karma arama işlemi bir saniyeden kısa sürer.

Write-Host'ı dikkatli kullanma

Komutun Write-Host yalnızca Başarılı işlem hattına nesne yazmak yerine konak konsoluna biçimlendirilmiş metin yazmanız gerektiğinde kullanılması gerekir.

Write-Host, powershell.exeveya powershell_ise.exegibi pwsh.exebelirli konaklara göre daha [Console]::WriteLine() yavaş bir büyüklük sırası olabilir. Ancak, [Console]::WriteLine() tüm konaklarda çalışacağı garanti değildir. Ayrıca, kullanılarak [Console]::WriteLine() yazılan çıkış tarafından Start-Transcriptbaşlatılan transkriptlere yazılamaz.

JIT derlemesi

PowerShell, yorumlanan bayt kodu için betik kodunu derler. PowerShell 3'te başlayarak, döngüde sürekli yürütülen kodlar için PowerShell, kodu yerel koda derleyerek tam zamanında (JIT) performansı artırabilir.

300'den az yönerge içeren döngüler JIT derlemesi için uygundur. Bundan büyük döngüler derlenemeyecek kadar maliyetlidir. Döngü 16 kez yürütürse betik arka planda JIT ile derlenmiş olur. JIT derlemesi tamamlandığında, yürütme derlenen koda aktarılır.

bir işleve tekrarlanan çağrılardan kaçının

İşlev çağırmak pahalı bir işlem olabilir. Uzun süre çalışan sıkı bir döngüde bir işlevi çağırıyorsanız, döngünün işlevin içinde taşınmasını göz önünde bulundurun.

Aşağıdaki örnekleri değerlendirin:

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

Temel döngü örneği, performans için temel çizgidir. İkinci örnek, rastgele sayı oluşturucuyu sıkı bir döngüde çağrılan bir işleve sarmalar. Üçüncü örnek, döngünün işlevinin içine taşınmasını gösterir. İşlev yalnızca bir kez çağrılır, ancak kod yine de aynı miktarda rastgele sayı oluşturur. Her örnek için yürütme sürelerindeki farka dikkat edin.

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

Cmdlet işlem hatlarını sarmalamaktan kaçının

Cmdlet'lerin çoğu ardışık söz dizimi ve işlem olan işlem hattı için uygulanır. Örneğin:

cmdlet1 | cmdlet2 | cmdlet3

Yeni işlem hattını başlatmak pahalı olabilir, bu nedenle bir cmdlet işlem hattını başka bir mevcut işlem hattına sarmalamaktan kaçınmalısınız.

Aşağıdaki örneği inceleyin. Dosya Input.csv 2100 satır içeriyor. komut Export-Csv işlem hattının ForEach-Object içine sarmalanır. Döngünün Export-Csv her yinelemesi için cmdlet çağrılır 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

Sonraki örnekte, Export-Csv komut işlem hattının ForEach-Object dışına taşındı. Bu durumda, Export-Csv yalnızca bir kez çağrılır, ancak yine de tarafından ForEach-Objectgeçirilen tüm nesneleri işler.

$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

Eşlenmemiş örnek 372 kat daha hızlıdır. Ayrıca, ilk uygulamanın sonraki uygulama için gerekli olmayan Append parametresini gerektirdiğine dikkat edin.

Dinamik olarak yeni nesneler oluşturmak için OrderedDictionary kullanma

Bazı girişlere göre dinamik olarak nesne oluşturmamız gerekebilecek durumlar olabilir. Bu, yeni bir PSObject oluşturmak ve ardından cmdlet'ini kullanarak Add-Member yeni özellikler eklemek için belki de en yaygın kullanılan yöntemdir. Bu tekniği kullanan küçük koleksiyonların performans maliyeti göz ardı edilebilir ancak büyük koleksiyonlar için çok belirgin hale gelebilir. Bu durumda, önerilen yaklaşım bir kullanmak [OrderedDictionary] ve ardından tür hızlandırıcısını kullanarak bir PSObject'e[pscustomobject] dönüştürmektir. Daha fazla bilgi için about_Hash_Tables'nin Sıralı sözlükler oluşturma bölümüne bakın.

değişkeninde $jsonaşağıdaki API yanıtının depolandığını varsayalım.

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

Şimdi, bu verileri bir CSV'ye aktarmak istediğinizi varsayalım. İlk olarak yeni nesneler oluşturmanız ve cmdlet'ini Add-Member kullanarak özellikleri ve değerleri eklemeniz gerekir.

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

kullanarak OrderedDictionarykod şu şekilde çevrilebilir:

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

Her iki durumda $result da çıkış aynı olacaktır:

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

İkinci yaklaşım, nesne sayısı ve üye özellikleri arttıkça üstel olarak daha verimli hale gelir.

5 özelliğe sahip nesne oluşturmaya yönelik üç tekniğin performans karşılaştırması aşağıdadır:

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

Sonuçlar şunlardır:

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