Pertimbangan performa pembuatan skrip PowerShell

Skrip PowerShell yang memanfaatkan .NET secara langsung dan menghindari alur cenderung lebih cepat daripada PowerShell idiomatic. Idiomatic PowerShell biasanya menggunakan cmdlet dan fungsi PowerShell sangat, sering memanfaatkan alur, dan turun 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:

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

Penugasan ke $null atau transmisi [void] kira-kira setara dan umumnya harus lebih disukai jika performa penting.

$arrayList.Add($item) > $null

Pengalihan file ke $null hampir sama baiknya dengan alternatif sebelumnya, sebagian besar skrip tidak akan pernah melihat perbedaannya. Tergantung pada skenarionya, pengalihan file memang memperkenalkan sedikit overhead.

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

Anda juga dapat menyalurkan ke Out-Null. Di PowerShell 7.x, ini sedikit lebih lambat daripada pengalihan tetapi mungkin tidak terlihat untuk sebagian besar skrip. Namun, memanggil Out-Null dalam perulangan besar bisa secara signifikan lebih lambat, bahkan di PowerShell 7.x.

$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 tidak memiliki pengoptimalan yang sama dengan Out-Null PowerShell 7.x, jadi Anda harus menghindari penggunaan Out-Null dalam kode sensitif performa.

Memperkenalkan blok skrip dan memanggilnya (menggunakan sumber titik atau sebaliknya) kemudian menetapkan hasilnya $null adalah teknik yang nyaman untuk menekan output blok besar skrip.

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

Teknik ini berkinerja kira-kira serta pipa ke Out-Null dan harus dihindari dalam skrip sensitif performa. Overhead ekstra dalam contoh ini berasal dari pembuatan dan pemanggilan blok skrip yang sebelumnya merupakan skrip sebaris.

Penambahan array

Membuat daftar item sering dilakukan menggunakan array dengan operator penambahan:

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

Ini bisa sangat tidak efisien karena array tidak dapat diubah. Setiap tambahan ke array benar-benar membuat array baru yang cukup besar untuk menahan semua elemen operand kiri dan kanan, lalu menyalin elemen kedua operan ke dalam array baru. Untuk koleksi kecil, overhead ini mungkin tidak masalah. Untuk koleksi besar, ini pasti bisa menjadi masalah.

Ada beberapa alternatif. Jika Anda tidak benar-benar memerlukan array, pertimbangkan untuk menggunakan ArrayList:

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

Jika Anda memerlukan array, Anda dapat menggunakan array Anda sendiri ArrayList dan cukup memanggil ArrayList.ToArray saat Anda menginginkan array. Atau, Anda dapat membiarkan PowerShell membuat ArrayList dan Array untuk Anda:

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

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

Penambahan string

Seperti array, string tidak dapat diubah. Setiap penambahan string benar-benar membuat string baru yang cukup besar untuk menahan konten operand kiri dan kanan, lalu menyalin elemen kedua operan ke dalam string baru. Untuk string kecil, overhead ini mungkin tidak masalah. Untuk string besar, ini pasti bisa menjadi masalah.

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

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

Ada beberapa alternatif. Anda dapat menggunakan -join operator untuk menggabungkan string.

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

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

Dalam contoh ini, menggunakan -join operator hampir 30 kali lebih cepat daripada penambahan string.

Anda juga dapat menggunakan kelas .NET StringBuilder .

$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

Dalam contoh ini, menggunakan StringBuilder hampir 50 kali lebih cepat daripada penambahan string.

Memproses file besar

Cara idiomatik untuk memproses file di PowerShell mungkin terlihat seperti:

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

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

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

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 besaran yang lebih lambat dari [Console]::WriteLine(). Namun, ketahuilah bahwa [Console]::WriteLine() hanya alternatif yang cocok untuk host tertentu seperti pwsh.exe, , powershell.exeatau powershell_ise.exe. Tidak dijamin untuk bekerja 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 dijalankan berulang kali 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.

Pertimbangkan 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