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.

  • $null'e atama veya dosya yönlendirmesi
  • [void]'a dönüştürme
  • boruya Out-Null

$null'a atama, [void]'a dönüştürme ve $null'ye dosya yönlendirme hızları neredeyse aynıdır. Ancak, özellikle PowerShell 5.1'de büyük bir döngüde Out-Null çağrı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

Not

PowerShell 7.5'te dizi ekleme iyileştirildi ve artık her işlem için yeni bir dizi oluşturmıyordu. Burada açıklanan performans konuları, 7.5 öncesi PowerShell sürümleri için de geçerlidir. Daha fazla bilgi için bkz. PowerShell 7.5'teki Yenilikler.

Diziler sabit bir boyuta sahip olduğundan dizi ekleme verimsizdir. Diziye her ekleme, sol ve 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. Diziye gerçekten ihtiyacınız yoksa, bunun yerine türlendirilmiş genel bir liste ([List<T>]) kullanmayı düşünün.

$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, dizi ekleme ve Add(T) nesnesinde [List<T>] yöntemini kullanarak bir diziye açıkça değer atamayı 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şlemi List<T>eklemekten çok daha yavaştır.

[List<T>] nesnesi kullanırken, listeyi [string] veya [int]gibi 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 dönüştürülemiyorsa, yöntem 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 [Object] ile 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, listeden ToArray() yöntemini ç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çindeki işlem hattına yazılan sonuçları tutmak için bir [ArrayList] oluşturur. $results atamasından hemen önce, PowerShell [ArrayList] öğesini bir [Object[]]'ye dönüştürür.

Tür açısından güvenli koleksiyonlar

PowerShell, kodlamayı kolaylaştıran ancak performans üzerindeki etkileri olabilecek, gevşek bir şekilde yazılan bir dildir. Tür güvenli (veya türe özgü) koleksiyonları kullanmayı göz önünde bulundurun. Tür açısından güvenli koleksiyonlar daha az bellek kullanır ve daha hızlıdır. Aşağıdaki örnekleri karşılaştırın:

$Stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
$ListInt = [System.Collections.Generic.List[int]]::new()
for ($i = 0; $i -lt 1mb; $i++) {
    $ListInt.Add($i)
}
$Stopwatch.Stop()
Write-Host "Time to add 1mb integers to List[int]: $($Stopwatch.Elapsed.TotalSeconds) seconds."
Time to add 1mb integers to List[int]: 9.8841501 seconds.

[int] listesi oluşturmak, [Object] listesi oluşturmaktan daha hızlıdır.

$Stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
$ListObject = [System.Collections.Generic.List[Object]]::new()
for ($i = 0; $i -lt 1mb; $i++) {
    $ListObject.Add($i)
}
$Stopwatch.Stop()
Write-Host "Time to add 1mb integers to List[Object]: $($Stopwatch.Elapsed.TotalSeconds) seconds."
Time to add 1mb integers to List[Object]: 10.5677782 seconds.

Dize ekleme

Dizeler değiştirilemezdir. 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ış, -join işlecinin en hızlı olduğunu ve ardından [StringBuilder] sınıfı olduğunu gösterir.

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 yerleşik bir yöntem şöyle görünebilir:

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

.NET API'lerini doğrudan kullanmaktan kat kat daha yavaş 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()
    }
}

Ayrıca, ReadLines'yi saran ve okuma işlemini basitleştiren [System.IO.File]StreamReader yöntemini 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. İkinci koleksiyonun tekrarlı filtrelenmesi özellikle büyük bir ek yük oluşturur.

Biri Kimliği ve Adı, diğeri Ad ve E-postaolan iki koleksiyon verilir:

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

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

Kimliği, Adve E-posta özellikleriyle nesnelerin listesini döndürmek için bu koleksiyonları uzlaştırmanın olağan yolu şu şekilde 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 $Accounts koleksiyonundaki her öğe için $Employee koleksiyonundaki 5000 öğenin tümünü bir kez filtrelemesi gerekir. Bu yalnızca tek bir değer için yapılan arama bile dakikalar sürebilir.

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

$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 bir saniyeden kısa sürede tamamlanır.

Write-Host dikkatle kullan

Write-Host komutu, Success işlem hattına nesne yazmak yerine yalnızca konak konsoluna biçimlendirilmiş metin yazmanız gerektiğinde kullanılmalıdır.

Write-Host için [Console]::WriteLine() ile karşılaştırıldığında, pwsh.exe, powershell.exe veya powershell_ise.exe gibi belirli konaklar için büyüklük derecesinde daha yavaş olabilir. Ancak [Console]::WriteLine() tüm konaklarda çalışacağı garanti değildir. Ayrıca, [Console]::WriteLine() kullanılarak yazılan çıkış, Start-Transcripttarafından baş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üldüğünde, betik arka planda JIT olarak derlenir. 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 yoğun bir döngüde bir işlevi çağırıyorsanız, döngüyü işlevin içine taşımayı düşünün.

Aşağıdaki örnekleri göz önünde bulundurun:

$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 for döngüsü örneği, performans için bir referans noktasıdır. İ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, sıralı sentaks ve işlem süreci olan boru hattı için uygulanmaktadır. Mesela:

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 göz önünde bulundurun. Input.csv dosyası 2100 satır içerir. Export-Csv komutu ForEach-Object işlem hattının içinde yer alır. Export-Csv döngüsünün her yinelemesi için ForEach-Object cmdlet'i çağrılır.

$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 komutu ForEach-Object işlem hattının dışına taşındı. Bu durumda, Export-Csv yalnızca bir kez çağrılır, ancak yine de ForEach-Object 'den geç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

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

Gereksiz koleksiyon numaralandırmasından kaçının

PowerShell karşılaştırma işleçleri, koleksiyonları karşılaştırırken bir kolaylık özelliğine sahiptir. İfadedeki soldaki değer bir koleksiyon olduğunda işleç, koleksiyonun ifadenin sağındaki değerle eşleşen öğelerini döndürür.

Bu özellik, bir koleksiyonu filtrelemek için basit bir yol sağlar. Mesela:

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

Ancak, yalnızca boole sonucu bekleyen bir koşullu deyimde koleksiyon karşılaştırması kullandığınızda, bu özellik düşük performansa neden olabilir.

Örneğin:

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

Bu örnekte PowerShell, sağ taraftaki değeri koleksiyondaki her değerle karşılaştırır ve bir sonuç koleksiyonu döndürür. Sonuç boş olmadığından null olmayan sonuç olarak $truedeğerlendirilir. İlk eşleşme bulunduğunda koşul doğrudur, ancak PowerShell yine de koleksiyonun tamamını numaralandırır. Bu numaralandırmanın büyük koleksiyonlar için önemli bir performans etkisi olabilir.

Performansı artırmanın bir yolu, koleksiyonun Where() yöntemini kullanmaktır. Yöntem, ilk eşleşmeyi bulduğunda koleksiyonu değerlendirmeyi durdurur.

# 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

Bir milyon öğe için yönteminin Where() kullanılması önemli ölçüde daha hızlıdır.

Nesne oluşturma

New-Object cmdlet'ini kullanarak nesne oluşturmak yavaş olabilir. Aşağıdaki kod, New-Object cmdlet'ini kullanarak nesne oluşturma performansını [pscustomobject] tür hızlandırıcısı ile karşılaştırır.

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, tüm .NET türleri için new() statik yöntemini ekledi. Aşağıdaki kod, New-Object cmdlet'ini kullanarak nesne oluşturma performansını new() yöntemiyle karşılaştırır.

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

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, psobject yeni bir oluşturmanın ve ardından cmdlet'ini kullanarak yeni özellikler eklemenin belki de en yaygın kullanılan yoludur. 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 [OrderedDictionary] kullanmak ve ardından tür hızlandırıcısını kullanarak bunu bir [pscustomobject]'e dönüştürmektir. Daha fazla bilgi için about_Hash_TablesSıralı sözlük oluşturma bölümüne bakın.

$jsondeğişkeninde aş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 Add-Member cmdlet'ini 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
}

OrderedDictionarykullanarak kod ş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 da $result çı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