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-Null
oluş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-Command
kullanarak ) 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 $results
hemen ö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.exe
veya powershell_ise.exe
gibi pwsh.exe
belirli 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-Transcript
başlatılan transkriptlere yazılamaz.
kullanmak Write-Host
yerine 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-Object
geç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.