PowerShell betik oluşturma performansında 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 cmdlet'leri ve PowerShell işlevlerini kullanır; genellikle işlem hattından yararlanılır ve yalnızca gerektiğinde .NET'e başvurur.

Not

Burada açıklanan tekniklerin çoğu idiomatic PowerShell değildir ve PowerShell betiğinin okunabilirliğini azaltabilir. Betik yazarlarının performans aksini belirtmediği sürece idiomatic PowerShell kullanmaları önerilir.

Çıktıyı gizleme

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

'a $null atama veya atama [void] kabaca eşdeğerdir ve performansın önemli olduğu durumlarda tercih edilmelidir.

$null = $arrayList.Add($item)
[void]$arrayList.Add($item)

'a $null dosya yeniden yönlendirme, önceki alternatifler kadar hızlıdır. Çoğu betik için performans farkı fark etmezsiniz. Ancak, dosya yeniden yönlendirme bazı ek yük getirir.

$arrayList.Add($item) > $null

öğesine de kanal Out-Nulloluşturabilirsiniz. PowerShell 7.x'te bu, yeniden yönlendirmeden biraz daha yavaştır, ancak betiklerin çoğu için büyük olasılıkla fark edilmez.

$arrayList.Add($item) | Out-Null

Ancak, PowerShell 7.x'te bile büyük bir döngüde çağrı Out-Null çok daha yavaş olabilir.

$d = Get-Date
Measure-Command { for($i=0; $i -lt 1mb; $i++) { $null=$d } } |
    Select-Object TotalSeconds

TotalSeconds
------------
   1.0549325

$d = Get-Date
Measure-Command { for($i=0; $i -lt 1mb; $i++) { $d | Out-Null } } |
    Select-Object TotalSeconds

TotalSeconds
------------
   5.9572186

Windows PowerShell 5.1, PowerShell 7.x ile Out-Null aynı iyileştirmelere sahip olmadığından performansa duyarlı kod kullanmaktan kaçınmanız Out-Null gerekir.

Betik bloğu oluşturup çağırmak (nokta kaynağını belirleme veya Invoke-Commandkullanarak ) ve ardından sonucun $null atanması, büyük bir betik bloğunun çıktısını gizlemeye yönelik kullanışlı bir tekniktir.

$null = . {
    $arrayList.Add($item)
    $arrayList.Add(42)
}

Bu teknik, 'a Out-Null boru ile yaklaşık olarak aynı işlemi yapar ve performansa duyarlı betikte bundan kaçınılmalıdır. Bu örnekteki ek yük, daha önce satır içi betik olan bir betik bloğunun oluşturulması ve çağrılmasından kaynaklandı.

Dizi ekleme

Öğe listesi oluşturma işlemi genellikle toplama işleciyle bir dizi kullanılarak gerçekleştirilir:

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

Diziler sabit bir boyuta sahip olduğundan dizi ekleme verimsizdir. Diziye yapılan 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 kötü olabilir.

Birkaç alternatif vardır. Aslında bir diziye ihtiyacınız yoksa, bunun yerine yazılan genel bir liste (Liste<T>) kullanmayı göz önünde bulundurun:

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

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

$tests = @{
    'PowerShell Explicit Assignment' = {
        param($count)

        $result = foreach($i in 1..$count) {
            $i
        }
    }
    '.Add(..) 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
        }
    }
}

5000, 10000, 100000 | 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'
        }
    }
}
CollectionSize Test                           TotalMilliseconds RelativeSpeed
-------------- ----                           ----------------- -------------
          5000 PowerShell Explicit Assignment              0.56 1x
          5000 .Add(..) to List<T>                         7.56 13.5x
          5000 += Operator to Array                     1357.74 2424.54x
         10000 PowerShell Explicit Assignment              0.77 1x
         10000 .Add(..) to List<T>                        18.20 23.64x
         10000 += Operator to Array                     5411.23 7027.57x
        100000 PowerShell Explicit Assignment             14.85 1x
        100000 .Add(..) to List<T>                       177.13 11.93x
        100000 += Operator to Array                   473824.71 31907.39x

Büyük koleksiyonlarla çalışırken, dizi ekleme bir Liste<T'ye> eklemeden çok daha yavaştır.

Liste<T> kullanırken, listeyi Dize veya Int gibi belirli bir türle oluşturmanız gerekir. Listeye farklı türde nesneler eklediğinizde, bunlar belirtilen türe türe türlenir. Belirtilen türe yayınlanamazlarsa, 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 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.GetEnumerator().ForEach({ "$_ 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 diziyi sizin için oluşturmasına izin vekleyebilirsiniz:

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

Bu örnekte PowerShell, dizi ifadesinin içinde işlem hattına yazılan sonuçları tutmak için bir ArrayList oluşturur. öğesine atanmadan $resultshemen önce, PowerShell ArrayList'i bir nesneye 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, ardından 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.

$string = ''
Measure-Command {
      foreach( $i in 1..10000)
      {
          $string += "Iteration $i`n"
      }
      $string
  } | Select-Object TotalMilliseconds

TotalMilliseconds
-----------------
         641.8168

Birkaç alternatif vardır. Dizeleri birleştirmek için işlecini -join kullanabilirsiniz.

Measure-Command {
      $string = @(
          foreach ($i in 1..10000) { "Iteration $i" }
      ) -join "`n"
      $string
  } | Select-Object TotalMilliseconds

TotalMilliseconds
-----------------
          22.7069

Bu örnekte, işlecini kullanmak dize toplamadan -join 30 kat daha hızlıdır.

.NET StringBuilder sınıfını da kullanabilirsiniz.

$sb = [System.Text.StringBuilder]::new()
Measure-Command {
      foreach( $i in 1..10000)
      {
          [void]$sb.Append("Iteration $i`n")
      }
      $sb.ToString()
  } | Select-Object TotalMilliseconds

TotalMilliseconds
-----------------
          13.4671

Bu örnekte StringBuilder'ın kullanılması, dize eklemeden 50 kat daha hızlıdır.

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:

try
{
    $stream = [System.IO.StreamReader]::new($path)
    while ($line = $stream.ReadLine())
    {
        if ($line.Length -gt 10)
        {
            $line
        }
    }
}
finally
{
    $stream.Dispose()
}

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ı koleksiyonlarda 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 filtrelemesi büyük bir ek yüke sahiptir.

Biri Kimlik ve Ad, diğeri Ad ve Email olan iki koleksiyon verilmiştir:

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

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

Kimlik, Ad ve Email özelliklerine sahip nesnelerin listesini döndürmek için bu koleksiyonları uzlaştırmanın normal 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 bu işlem 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 kaçının

Çıkışı doğrudan konsola yazmak genellikle kötü bir uygulama olarak kabul edilir, ancak mantıklı olduğunda birçok betik kullanır Write-Host.

Konsola çok sayıda ileti yazmanız gerekiyorsa, Write-Host , powershell.exeveya powershell_ise.exegibi pwsh.exebelirli konaklara göre daha [Console]::WriteLine() yavaş bir sıra 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.

kullanmak Write-Hostyerine Write-Output kullanmayı göz önünde bulundurun.

JIT derlemesi

PowerShell, betik kodunu yorumlanan bayt kodu olarak 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ı geliştirebilir.

300'den az yönerge içeren döngüler JIT derlemesi için uygundur. Bundan daha 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. Bir işlevi uzun süre çalışan sıkı bir döngüde çağırıyorsanız, döngünün işlevin içine taşınmasını göz önünde bulundurun.

Aşağıdaki örnekleri inceleyin:

$ranGen = New-Object System.Random
$RepeatCount = 10000

'Basic for-loop = {0}ms' -f (Measure-Command -Expression {
    for ($i = 0; $i -lt $RepeatCount; $i++) {
        $Null = $ranGen.Next()
    }
}).TotalMilliseconds

'Wrapped in a function = {0}ms' -f (Measure-Command -Expression {
    function Get-RandNum_Core {
        param ($ranGen)
        $ranGen.Next()
    }

    for ($i = 0; $i -lt $RepeatCount; $i++) {
        $Null = Get-RandNum_Core $ranGen
    }
}).TotalMilliseconds

'For-loop in a function = {0}ms' -f (Measure-Command -Expression {
    function Get-RandNum_All {
        param ($ranGen)
        for ($i = 0; $i -lt $RepeatCount; $i++) {
            $Null = $ranGen.Next()
        }
    }

    Get-RandNum_All $ranGen
}).TotalMilliseconds

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ı sağlar. İşlev yalnızca bir kez çağrılır, ancak kod yine de 10000 rastgele sayı oluşturur. Her örnek için yürütme sürelerindeki farka dikkat edin.

Basic for-loop = 47.8668ms
Wrapped in a function = 820.1396ms
For-loop in a function = 23.3193ms

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

Çoğu cmdlet ardışık söz dizimi ve işlem olan işlem hattı için uygulanır. Örnek:

cmdlet1 | cmdlet2 | cmdlet3

Yeni bir 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çerir. 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 .

'Wrapped = {0:N2} ms' -f (Measure-Command -Expression {
    Import-Csv .\Input.csv | ForEach-Object -Begin { $Id = 1 } -Process {
        [PSCustomObject]@{
            Id = $Id
            Name = $_.opened_by
        } | Export-Csv .\Output1.csv -Append
    }
}).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 dışında ForEach-Objectgeçirilen tüm nesneleri işler.

'Unwrapped = {0:N2} ms' -f (Measure-Command -Expression {
      Import-Csv .\Input.csv | ForEach-Object -Begin { $Id = 2 } -Process {
          [PSCustomObject]@{
              Id = $Id
              Name = $_.opened_by
          }
      } | Export-Csv .\Output2.csv
  }).TotalMilliseconds

Unwrapped = 42.92 ms

Eşleşmeyen ö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.