Bagikan melalui


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.

  • Penugasan atau pengalihan file ke $null
  • Transmisi ke [void]
  • 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 += Get-Something
$results += Get-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 ([List<T>]):

$results = [System.Collections.Generic.List[object]]::new()
$results.AddRange((Get-Something))
$results.AddRange((Get-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(T) metode pada [List<T>] objek. Ini mendefinisikan penetapan eksplisit sebagai garis besar untuk performa.

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

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(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

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

Saat menggunakan [List<T>] objek, 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 berbagai jenis objek, buat dengan [Object] 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 | ForEach-Object { "$_ 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 = @(
    Get-Something
    Get-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 [ArrayList] [Object[]].

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 11 di PowerShell 7.4.2. Output menunjukkan bahwa -join operator adalah yang tercepat, diikuti oleh [StringBuilder] kelas .

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

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. Misalnya, Anda dapat menggunakan kelas .NET [StreamReader] :

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()
    }
}

Anda juga dapat menggunakan ReadLines metode [System.IO.File], yang membungkus StreamReader, menyederhanakan proses membaca:

foreach ($line in [System.IO.File]::ReadLines($path)) {
    if ($line.Length -gt 10) {
        $line
    }
}

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.

Gunakan Write-Host dengan hati-hati

Perintah Write-Host hanya boleh digunakan ketika Anda perlu menulis teks yang diformat ke konsol host, daripada menulis objek ke alur Sukses .

Write-Host bisa menjadi urutan besaran yang 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.

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:

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

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 jumlah angka acak yang sama. Perhatikan perbedaan waktu eksekusi untuk setiap contoh.

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

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.

$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

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.

$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

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

Menggunakan OrderedDictionary untuk membuat objek baru secara dinamis

Ada situasi di mana kita mungkin perlu membuat objek secara dinamis berdasarkan beberapa input, cara yang mungkin paling umum digunakan untuk membuat PSObject baru dan kemudian menambahkan properti baru menggunakan Add-Member cmdlet. Biaya performa untuk koleksi kecil menggunakan teknik ini mungkin dapat diabaikan namun dapat menjadi sangat terlihat untuk koleksi besar. Dalam hal ini, pendekatan yang direkomendasikan adalah menggunakan [OrderedDictionary] dan kemudian mengonversinya ke PSObject menggunakan [pscustomobject] akselerator jenis. Untuk informasi selengkapnya, lihat bagian Membuat kamus yang diurutkan dari about_Hash_Tables.

Asumsikan Anda memiliki respons API berikut yang disimpan dalam variabel $json.

{
  "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" ]
      ]
    }
  ]
}

Sekarang, misalkan Anda ingin mengekspor data ini ke CSV. Pertama, Anda perlu membuat objek baru dan menambahkan properti dan nilai menggunakan Add-Member cmdlet.

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

OrderedDictionaryMenggunakan , kode dapat diterjemahkan ke:

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

Dalam kedua kasus $result , output akan sama:

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

Pendekatan terakhir menjadi secara eksponensial lebih efisien saat jumlah objek dan properti anggota meningkat.

Berikut adalah perbandingan performa tiga teknik untuk membuat objek dengan 5 properti:

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

Dan ini adalah hasilnya:

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