Поделиться через


Вопросы производительности сценариев PowerShell

Сценарии PowerShell, которые используют .NET напрямую в обход конвейера, часто работают быстрее, чем идиоматический код PowerShell. Idiomatic PowerShell использует командлеты и функции PowerShell, часто используя конвейер и прибегая к .NET только при необходимости.

Примечание.

Многие описанные здесь методы не идиоматичны PowerShell и могут снизить удобочитаемость скрипта PowerShell. Если жесткие требования к производительности отсутствуют, разработчикам сценариев рекомендуется следовать идиоматике PowerShell.

Подавление вывода

Существует множество способов избежать записи объектов в конвейер.

  • Назначение или перенаправление файлов в $null
  • Приведение к [void]
  • Канал в Out-Null

Скорость назначения $null, приведение к [void]и перенаправлению $null файлов практически идентичны. Однако вызов Out-Null в большом цикле может быть значительно медленнее, особенно в 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'
        }
    }
}

Эти тесты были запущены на компьютере с Windows 11 в PowerShell 7.3.4. Результаты показаны ниже.

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

Время и относительные скорости могут отличаться в зависимости от оборудования, версии PowerShell и текущей рабочей нагрузки в системе.

Сложение массивов

Создание списка элементов часто выполняется с помощью массива с оператором сложения:

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

Добавление массива неэффективно, так как массивы имеют фиксированный размер. Каждое дополнение к массиву создает достаточно большой массив для хранения всех элементов как левых, так и правых операндов. Элементы обоих операндов копируются в новый массив. Для небольших коллекций такая дополнительная нагрузка может не иметь значения. Производительность может страдать от больших коллекций.

Существует пара альтернативных вариантов. Если на самом деле не требуется массив, рекомендуется использовать типизированный универсальный список ([List<T>]):

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

Влияние на производительность добавления массива увеличивается экспоненциально с размером коллекции и числами. Этот код сравнивает явное назначение значений массиву с использованием добавления массива и использования Add(T) метода в объекте [List<T>] . Он определяет явное назначение в качестве базового показателя производительности.

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

Эти тесты были запущены на компьютере с Windows 11 в 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

При работе с большими коллекциями добавление массива значительно медленнее, чем добавление в .List<T>

При использовании [List<T>] объекта необходимо создать список с определенным типом, например [String] или [Int]. При добавлении объектов другого типа в список они приведение к указанному типу. Если они не могут быть приведение к указанному типу, метод вызывает исключение.

$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

Если вам нужно, чтобы список был коллекцией различных типов объектов, создайте его с [Object] типом списка. Вы можете перечислить коллекцию, чтобы проверить типы объектов в нем.

$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

Если требуется массив, можно вызвать ToArray() метод в списке или позволить PowerShell создать массив для вас:

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

В этом примере PowerShell создает [ArrayList] для хранения результатов, которые записываются в конвейер в выражении массива. Непосредственно перед тем, как назначать $results, PowerShell преобразует [ArrayList] в [Object[]].

Добавление строк

Строки являются неизменяемыми. Каждое добавление к строке фактически создает новую строку, достаточно большую для хранения содержимого левого и правого операндов, а затем копирует элементы обоих операндов в эту новую строку. Для небольших строк такая дополнительная нагрузка может не иметь значения. Для больших строк это может повлиять на производительность и потребление памяти.

Существует по крайней мере два варианта:

  • Оператор -join объединяет строки
  • Класс .NET [StringBuilder] предоставляет изменяемую строку

В следующем примере сравнивается производительность этих трех методов построения строки.

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

Эти тесты были запущены на компьютере с Windows 11 в PowerShell 7.4.2. Выходные данные показывают, что -join оператор является самым быстрым, а затем классом [StringBuilder] .

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

Время и относительные скорости могут отличаться в зависимости от оборудования, версии PowerShell и текущей рабочей нагрузки в системе.

Обработка больших файлов

Идиоматическая обработка файла в PowerShell выглядит примерно так:

Get-Content $path | Where-Object Length -GT 10

Это может быть более медленным порядком, чем использование API .NET напрямую. Например, можно использовать класс .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()
    }
}

Кроме того, можно использовать ReadLines метод оболочки[System.IO.File]StreamReader, упрощающий процесс чтения:

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

Поиск записей по свойству в больших коллекциях

Обычно необходимо использовать общее свойство для идентификации одной записи в разных коллекциях, например с помощью имени для получения идентификатора из одного списка и электронной почты из другого. Итерации по первому списку, чтобы найти соответствующую запись во второй коллекции, медленно. В частности, повторная фильтрация второй коллекции имеет большие затраты.

Учитывая две коллекции, одна с идентификатором и именем, другая с именем и адресом электронной почты:

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

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

Обычный способ примирить эти коллекции для возврата списка объектов с идентификатором, именем и свойством электронной почты может выглядеть следующим образом:

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

Однако эта реализация должна фильтровать все 5000 элементов в $Accounts коллекции один раз для каждого элемента в $Employee коллекции. Это может занять несколько минут, даже для поиска с одним значением.

Вместо этого можно сделать хэш-таблицу , которая использует общее свойство Name в качестве ключа и соответствующую учетную запись в качестве значения.

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

Поиск ключей в хэш-таблице гораздо быстрее, чем фильтрация коллекции по значениям свойств. Вместо проверка каждого элемента в коллекции PowerShell может проверка, если ключ определен и использует его значение.

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

Это гораздо быстрее. Пока фильтр циклов занимает несколько минут, хэш-поиск занимает менее секунды.

Тщательно используйте узел записи

Команда Write-Host должна использоваться только в том случае, если необходимо написать форматированный текст в консоль узла, а не писать объекты в конвейер Success .

Write-Host может быть порядком медленнее, чем [Console]::WriteLine() для определенных узлов, таких как pwsh.exe, powershell.exeили powershell_ise.exe. Однако не гарантируется, [Console]::WriteLine() что он работает во всех узлах. Кроме того, выходные данные, записанные с помощью [Console]::WriteLine() , не записываются в расшифровки, запущенные Start-Transcript.

Компиляция JIT

PowerShell компилирует код скрипта в байт-код, который интерпретируется. Начиная с PowerShell 3, для кода, который многократно выполняется в цикле, PowerShell может повысить производительность, скомпилируя код в машинный код.

Циклы, у которых менее 300 инструкций, подходят для JIT-компиляции. Компиляция циклов большего размера обходится слишком дорого. Если цикл был выполнен 16 раз, скрипт компилируется JIT-компилятором в фоновом режиме. После завершения JIT-компиляции выполнение передается в компилированный код.

Избегайте повторных вызовов функции

Вызов функции может потребовать значительных ресурсов. Если вы вызываете функцию в длительном жестком цикле, рассмотрите возможность перемещения цикла внутри функции.

Рассмотрим следующие примеры:

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

Базовый пример цикла — это база для повышения производительности. Второй пример упаковывает генератор случайных чисел в функцию, которая вызывается в жестком цикле. В третьем примере цикл перемещается внутри функции. Функция вызывается только один раз, но код по-прежнему создает то же количество случайных чисел. Обратите внимание на разницу во времени выполнения для каждого примера.

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

Избегайте упаковки конвейеров командлетов

Большинство командлетов реализованы для конвейера, который является последовательным синтаксисом и процессом. Например:

cmdlet1 | cmdlet2 | cmdlet3

Инициализация нового конвейера может быть дорогой, поэтому следует избегать упаковки конвейера командлета в другой существующий конвейер.

Рассмотрим следующий пример. Файл Input.csv содержит 2100 строк. Команда Export-Csv упаковывается в ForEach-Object конвейер. Export-Csv Командлет вызывается для каждой ForEach-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

В следующем примере Export-Csv команда была перемещена за пределы конвейера ForEach-Object . В этом случае Export-Csv вызывается только один раз, но все равно обрабатывает все объекты, передаваемые 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

Пример распакованного фрагмента составляет 372 раза быстрее. Кроме того, обратите внимание, что для первой реализации требуется параметр Append, который не требуется для последующей реализации.

Использование OrderedDictionary для динамического создания новых объектов

Существуют ситуации, когда может потребоваться динамически создавать объекты на основе некоторых входных данных, возможно, наиболее часто используемый способ создания нового PSObject и добавления новых свойств с помощью командлетаAdd-Member. Затраты на производительность небольших коллекций, использующих этот метод, могут быть незначительными, однако это может стать очень заметным для больших коллекций. В этом случае рекомендуется использовать [OrderedDictionary] и преобразовать его в PSObject с помощью [pscustomobject] акселератора типов. Дополнительные сведения см. в разделе "Создание упорядоченных словарей" about_Hash_Tables.

Предположим, что у вас есть следующий ответ API, хранящийся в переменной $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" ]
      ]
    }
  ]
}

Предположим, что вы хотите экспортировать эти данные в CSV-файл. Сначала необходимо создать новые объекты и добавить свойства и значения с помощью командлета Add-Member .

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

OrderedDictionaryС помощью кода можно преобразовать в:

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

В обоих случаях выходные $result данные будут одинаковыми:

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

Последний подход становится экспоненциально более эффективным по мере увеличения числа объектов и свойств элементов.

Ниже приведено сравнение производительности трех методов создания объектов с 5 свойствами:

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

И это результаты:

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