Pertimbangan performa pembuatan skrip PowerShell

Skrip PowerShell yang memanfaatkan .NET secara langsung dan menghindari alur cenderung lebih cepat daripada PowerShell idiomatik. Idiomatic PowerShell menggunakan cmdlet dan fungsi PowerShell, sering memanfaatkan alur, dan menggunakan ke .NET hanya jika perlu.

Catatan

Banyak teknik yang dijelaskan di sini bukan PowerShell idiomatik dan dapat mengurangi keterbacaan skrip PowerShell. Penulis skrip disarankan untuk menggunakan PowerShell idiomatik kecuali performa menentukan sebaliknya.

Menekan output

Ada banyak cara untuk menghindari penulisan objek ke alur.

  • Menetapkan ke $null
  • Transmisi ke [void]
  • Pengalihan file ke $null
  • Pipa ke Out-Null

Kecepatan penugasan ke $null, transmisi ke [void], dan pengalihan file hampir $null identik. Namun, panggilan Out-Null dalam perulangan besar bisa secara signifikan lebih lambat, terutama di 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'
        }
    }
}

Pengujian ini dijalankan pada komputer Windows 11 di PowerShell 7.3.4. Hasilnya ditunjukkan di bawah ini:

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

Waktu dan kecepatan relatif dapat bervariasi tergantung pada perangkat keras, versi PowerShell, dan beban kerja saat ini pada sistem.

Penambahan array

Membuat daftar item sering dilakukan menggunakan array dengan operator penambahan:

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

Penambahan array tidak efisien karena array memiliki ukuran tetap. Setiap tambahan ke array membuat array baru yang cukup besar untuk menahan semua elemen operand kiri dan kanan. Elemen kedua operand disalin ke dalam array baru. Untuk koleksi kecil, overhead ini mungkin tidak masalah. Performa dapat menderita untuk koleksi besar.

Ada beberapa alternatif. Jika Anda tidak benar-benar memerlukan array, pertimbangkan untuk menggunakan daftar generik yang ditik (Daftar<T>):

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

Dampak performa penggunaan penambahan array tumbuh secara eksponensial dengan ukuran koleksi dan penambahan angka. Kode ini membandingkan secara eksplisit menetapkan nilai ke array dengan menggunakan penambahan array dan menggunakan Add() metode pada Daftar<T>. Ini mendefinisikan penetapan eksplisit sebagai garis besar untuk performa.

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

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

Pengujian ini dijalankan pada komputer Windows 11 di PowerShell 7.3.4.

CollectionSize Test                           TotalMilliseconds RelativeSpeed
-------------- ----                           ----------------- -------------
          5120 PowerShell Explicit Assignment             26.65 1x
          5120 .Add(..) to List<T>                       110.98 4.16x
          5120 += Operator to Array                      402.91 15.12x
         10240 PowerShell Explicit Assignment              0.49 1x
         10240 .Add(..) to List<T>                       137.67 280.96x
         10240 += Operator to Array                     1678.13 3424.76x
        102400 PowerShell Explicit Assignment             11.18 1x
        102400 .Add(..) to List<T>                      1384.03 123.8x
        102400 += Operator to Array                   201991.06 18067.18x

Saat Anda bekerja dengan koleksi besar, penambahan array secara dramatis lebih lambat daripada menambahkan ke Daftar<T>.

Saat menggunakan Daftar<T>, Anda perlu membuat daftar dengan jenis tertentu, seperti String atau Int. Saat Anda menambahkan objek dari jenis yang berbeda ke daftar, objek tersebut ditransmisikan ke jenis yang ditentukan. Jika mereka tidak dapat ditransmisikan ke jenis yang ditentukan, metode akan menimbulkan pengecualian.

$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

Saat Anda memerlukan daftar untuk menjadi kumpulan jenis objek yang berbeda, buatlah dengan Objek sebagai jenis daftar. Anda dapat menghitung koleksi memeriksa jenis objek di dalamnya.

$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

Jika Anda memerlukan array, Anda dapat memanggil ToArray() metode pada daftar atau Anda dapat membiarkan PowerShell membuat array untuk Anda:

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

Dalam contoh ini, PowerShell membuat ArrayList untuk menahan hasil yang ditulis ke alur di dalam ekspresi array. Tepat sebelum menetapkan ke $results, PowerShell mengonversi ArrayList menjadi objek[].

Penambahan string

String tidak dapat diubah. Setiap penambahan pada string benar-benar membuat string baru yang cukup besar untuk menahan konten operand kiri dan kanan, lalu menyalin elemen kedua operand ke dalam string baru. Untuk string kecil, overhead ini mungkin tidak masalah. Untuk string besar, ini dapat memengaruhi performa dan konsumsi memori.

Setidaknya ada dua alternatif:

  • Operator -join menggabungkan string
  • Kelas .NET StringBuilder menyediakan string yang dapat diubah

Contoh berikut membandingkan performa ketiga metode membangun string ini.

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

Pengujian ini dijalankan pada komputer Windows 10 di PowerShell 7.3.4. Output menunjukkan bahwa -join operator adalah yang tercepat, diikuti oleh kelas StringBuilder .

Iterations Test                   TotalMilliseconds RelativeSpeed
---------- ----                   ----------------- -------------
     10240 Join operator                       7.08 1x
     10240 StringBuilder                      54.10 7.64x
     10240 Addition Assignment +=            724.16 102.28x
     51200 Join operator                      41.76 1x
     51200 StringBuilder                     318.06 7.62x
     51200 Addition Assignment +=          17693.06 423.68x
    102400 Join operator                     106.98 1x
    102400 StringBuilder                     543.84 5.08x
    102400 Addition Assignment +=          90693.13 847.76x

Waktu dan kecepatan relatif dapat bervariasi tergantung pada perangkat keras, versi PowerShell, dan beban kerja saat ini pada sistem.

Memproses file besar

Cara idiomatik untuk memproses file di PowerShell mungkin terlihat seperti:

Get-Content $path | Where-Object { $_.Length -gt 10 }

Ini bisa menjadi urutan besarnya lebih lambat daripada menggunakan API .NET secara langsung:

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

Mencari entri menurut properti dalam koleksi besar

Biasanya perlu menggunakan properti bersama untuk mengidentifikasi rekaman yang sama dalam koleksi yang berbeda, seperti menggunakan nama untuk mengambil ID dari satu daftar dan email dari daftar lain. Iterasi di atas daftar pertama untuk menemukan rekaman yang cocok di koleksi kedua lambat. Secara khusus, pemfilteran berulang koleksi kedua memiliki overhead besar.

Mengingat dua koleksi, satu dengan ID dan Nama, yang lain dengan Nama dan Email:

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

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

Cara biasa untuk mendamaikan koleksi ini untuk mengembalikan daftar objek dengan properti ID, Nama, dan Email mungkin terlihat seperti ini:

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

Namun, implementasi tersebut harus memfilter semua 5000 item dalam $Accounts koleksi sekali untuk setiap item dalam $Employee koleksi. Itu bisa memakan waktu beberapa menit, bahkan untuk pencarian nilai tunggal ini.

Sebagai gantinya, Anda dapat membuat tabel hash yang menggunakan properti Nama bersama sebagai kunci dan akun yang cocok sebagai nilai.

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

Mencari kunci dalam tabel hash jauh lebih cepat daripada memfilter koleksi menurut nilai properti. Alih-alih memeriksa setiap item dalam koleksi, PowerShell dapat memeriksa apakah kunci ditentukan dan menggunakan nilainya.

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

Ini jauh lebih cepat. Sementara filter perulangan membutuhkan waktu beberapa menit untuk diselesaikan, pencarian hash membutuhkan waktu kurang dari satu detik.

Hindari Write-Host

Umumnya dianggap sebagai praktik yang buruk untuk menulis output langsung ke konsol, tetapi ketika masuk akal, banyak skrip menggunakan Write-Host.

Jika Anda harus menulis banyak pesan ke konsol, Write-Host bisa menjadi urutan besarnya lebih lambat daripada [Console]::WriteLine() untuk host tertentu seperti pwsh.exe, , powershell.exeatau powershell_ise.exe. Namun, [Console]::WriteLine() tidak dijamin berfungsi di semua host. Selain itu, output yang ditulis menggunakan [Console]::WriteLine() tidak ditulis ke transkrip yang dimulai oleh Start-Transcript.

Alih-alih menggunakan Write-Host, pertimbangkan untuk menggunakan Write-Output.

Kompilasi JIT

PowerShell mengkompilasi kode skrip ke bytecode yang ditafsirkan. Dimulai di PowerShell 3, untuk kode yang berulang kali dijalankan dalam perulangan, PowerShell dapat meningkatkan performa dengan Just-in-time (JIT) yang mengkompilasi kode ke dalam kode asli.

Perulangan yang memiliki kurang dari 300 instruksi memenuhi syarat untuk kompilasi JIT. Perulangan yang lebih besar dari itu terlalu mahal untuk dikompilasi. Ketika perulangan telah dijalankan 16 kali, skrip dikompilasi JIT di latar belakang. Ketika kompilasi JIT selesai, eksekusi ditransfer ke kode yang dikompilasi.

Hindari panggilan berulang ke fungsi

Memanggil fungsi bisa menjadi operasi yang mahal. Jika Anda memanggil fungsi dalam perulangan ketat yang berjalan lama, pertimbangkan untuk memindahkan perulangan di dalam fungsi.

Perhatikan contoh berikut:

$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

Contoh dasar untuk perulangan adalah garis dasar untuk performa. Contoh kedua membungkus generator angka acak dalam fungsi yang disebut dalam perulangan yang ketat. Contoh ketiga memindahkan perulangan di dalam fungsi. Fungsi ini hanya dipanggil sekali tetapi kode masih menghasilkan 10000 angka acak. Perhatikan perbedaan waktu eksekusi untuk setiap contoh.

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

Hindari membungkus alur cmdlet

Sebagian besar cmdlet diimplementasikan untuk alur, yang merupakan sintaks dan proses berurutan. Contohnya:

cmdlet1 | cmdlet2 | cmdlet3

Menginisialisasi alur baru bisa mahal, oleh karena itu Anda harus menghindari pembungkusan alur cmdlet ke dalam alur lain yang ada.

Pertimbangkan contoh berikut. File Input.csv berisi 2100 baris. Export-Csv Perintah dibungkus di ForEach-Object dalam alur. Export-Csv Cmdlet dipanggil untuk setiap iterasi perulanganForEach-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

Untuk contoh berikutnya, Export-Csv perintah dipindahkan ke luar ForEach-Object alur. Dalam hal ini, Export-Csv hanya dipanggil sekali, tetapi masih memproses semua objek yang dilewatkan dari ForEach-Object.

'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

Contoh yang tidak dibungkus adalah 372 kali lebih cepat. Selain itu, perhatikan bahwa implementasi pertama memerlukan parameter Tambahkan , yang tidak diperlukan untuk implementasi nanti.