Considerações sobre o desempenho de script do PowerShell
Os scripts do PowerShell que aproveitam o .NET diretamente e evitam o pipeline tendem a ser mais rápidos do que o idiomático PowerShell. O PowerShell idiomático usa cmdlets e funções do PowerShell, geralmente aproveitando o pipeline e recorrendo ao .NET somente quando necessário.
Observação
Muitas das técnicas descritas aqui não usam o idiomático PowerShell e podem reduzir a legibilidade de um script do PowerShell. Autores de script são aconselhados a usar o idiomático PowerShell, a menos que o desempenho exija o contrário.
Suprimir saída
Há várias maneiras de se evitar a gravação de objetos no pipeline.
- Atribuição ou redirecionamento de arquivo para
$null
- Como converter para
[void]
- Pipe para
Out-Null
As velocidades de atribuição para $null
, conversão para[void]
e redirecionamento de arquivo para $null
são quase idênticas. No entanto, chamar Out-Null
em um loop grande pode ser significativamente mais lento, especialmente no 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'
}
}
}
Esses testes foram executados em um computador Windows 11 com o PowerShell 7.3.4. Os resultados são mostrados abaixo:
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
Os tempos e as velocidades relativas podem variar dependendo do hardware, da versão do PowerShell e da carga de trabalho atual no sistema.
Adição de matriz
A geração de uma lista de itens geralmente é feita usando uma matriz com o operador de adição:
$results = @()
$results += Get-Something
$results += Get-SomethingElse
$results
A adição de matriz é ineficiente porque as matrizes têm um tamanho fixo. Cada adição à matriz cria uma matriz grande o suficiente para conter todos os elementos dos operandos esquerdo e direito. Os elementos de ambos os operandos são copiados para a nova matriz. Para pequenas coleções, essa sobrecarga pode não ser importante. O desempenho pode sofrer para grandes coleções.
Há duas alternativas. Se você realmente não precisa de uma matriz, considere usar uma lista genérica digitada ([List<T>]
):
$results = [System.Collections.Generic.List[object]]::new()
$results.AddRange((Get-Something))
$results.AddRange((Get-SomethingElse))
$results
O impacto no desempenho do uso da adição de matriz aumenta exponencialmente com o tamanho da coleção e as adições numéricas. Esse código compara a atribuição explícita de valores a uma matriz com o uso da adição de matriz e o uso do método Add(T)
em um objeto [List<T>]
. Ele define a atribuição explícita como a linha de base para o desempenho.
$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'
}
}
}
Esses testes foram executados em um computador Windows 11 com o 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
Quando você trabalha com coleções grandes, a adição de matriz é dramaticamente mais lenta do que a adição a um List<T>
.
Ao usar um objeto [List<T>]
, você precisa criar a lista com um tipo específico, como [String]
ou [Int]
. Quando você adiciona objetos de um tipo diferente à lista, eles são convertidos para o tipo especificado. Se eles não puderem ser convertidos no tipo especificado, o método gerará uma exceção.
$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
Quando você precisar que a lista seja uma coleção de diferentes tipos de objetos, crie-a com [Object]
como tipo de lista. Você pode enumerar a coleção inspecionando os tipos dos objetos nela.
$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
Se você precisar de uma matriz, poderá chamar o método ToArray()
na lista ou permitir que o PowerShell crie a matriz para você:
$results = @(
Get-Something
Get-SomethingElse
)
Neste exemplo, o PowerShell cria uma [ArrayList]
para armazenar os resultados gravados no pipeline dentro da expressão de matriz. Justo antes de atribuir a $results
, o PowerShell converte [ArrayList]
em [Object[]]
.
Adição de cadeia de caracteres
As cadeias de caracteres são imutáveis. Cada adição à cadeia de caracteres, na verdade, cria uma cadeia de caracteres grande o suficiente para conter todos os elementos dos operandos esquerdo e direito e copia os elementos dos dois operandos para a nova cadeia de caracteres. Para cadeias de caracteres pequenas, essa sobrecarga pode não ser importante. Para cadeias de caracteres grandes, isso pode afetar o desempenho e o consumo de memória.
Há pelo menos duas alternativas:
- O
-join
operador concatena cadeia de caracteres - A classe .NET
[StringBuilder]
fornece uma cadeia de caracteres mutável
O exemplo a seguir compara o desempenho desses três métodos de criação de uma cadeia de caracteres.
$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'
}
}
}
Esses testes foram executados em uma máquina Windows 11 no PowerShell 7.4.2. A saída mostra que o operador -join
é o mais rápido, seguido pela classe [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
Os tempos e as velocidades relativas podem variar dependendo do hardware, da versão do PowerShell e da carga de trabalho atual no sistema.
Processar arquivos grandes
A maneira idiomática de processar um arquivo no PowerShell pode se assemelhar a:
Get-Content $path | Where-Object Length -GT 10
Isso pode ser muito mais lento do que usar APIs .NET diretamente. Por exemplo, você pode usar a classe .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()
}
}
Você também pode usar o método ReadLines
de [System.IO.File]
, que envolve StreamReader
, simplificando o processo de leitura:
foreach ($line in [System.IO.File]::ReadLines($path)) {
if ($line.Length -gt 10) {
$line
}
}
Pesquisar entradas por propriedade em coleções grandes
É comum precisar usar uma propriedade compartilhada para identificar o mesmo registro em coleções diferentes, como usar um nome para recuperar uma ID de uma lista e um email de outra. É um processo lento iterar na primeira lista para localizar o registro correspondente na segunda coleção. Especificamente, a filtragem repetida da segunda coleção tem uma grande sobrecarga.
Dadas duas coleções, uma com ID e Nome e outra com Nome e Email:
$Employees = 1..10000 | ForEach-Object {
[PSCustomObject]@{
Id = $_
Name = "Name$_"
}
}
$Accounts = 2500..7500 | ForEach-Object {
[PSCustomObject]@{
Name = "Name$_"
Email = "Name$_@fabrikam.com"
}
}
A maneira usual de reconciliar essas coleções para retornar uma lista de objetos com as propriedades ID, Nome e Email pode ser assim:
$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
}
}
No entanto, essa implementação precisa filtrar todos os 5.000 itens na coleção $Accounts
uma vez para cada item da coleção $Employee
. Isso pode levar minutos, mesmo nessa pesquisa de valor único.
Em vez disso, você pode criar uma Tabela Hash que usa a propriedade Nome compartilhada como chave e a conta correspondente como valor.
$LookupHash = @{}
foreach ($Account in $Accounts) {
$LookupHash[$Account.Name] = $Account
}
A procura de chaves em uma tabela de hash é muito mais rápida do que a filtragem de uma coleção por valores de propriedade. Em vez de verificar todos os itens da coleção, o PowerShell pode verificar se a chave está definida e usar seu valor.
$Results = $Employees | ForEach-Object -Process {
$Email = $LookupHash[$_.Name].Email
[pscustomobject]@{
Id = $_.Id
Name = $_.Name
Email = $Email
}
}
Isso é muito mais rápido. Embora o filtro de loop leve minutos para ser concluído, a pesquisa de hash leva menos de um segundo.
Use Write-Host com cuidado
O comando Write-Host
só deve ser usado quando você precisar gravar texto formatado no console do host, em vez de gravar objetos no pipeline Sucesso.
Write-Host
pode ser uma ordem de magnitude mais lenta que [Console]::WriteLine()
para hosts específicos comopwsh.exe
, powershell.exe
, ou powershell_ise.exe
. Entretanto, não há garantia de que [Console]::WriteLine()
funcione em todos os hosts. Além disso, a saída por escrito usando [Console]::WriteLine()
não é escrita em transcrições iniciadas por Start-Transcript
.
compilação JIT
O PowerShell compila o código de script para o código de bytes que é interpretado. A partir do PowerShell 3, para o código que é executado repetidamente em um loop, o PowerShell pode melhorar o desempenho ao realizar a compilação JIT (just-in-time) do código para código nativo.
Loops que têm menos de 300 instruções estão qualificados para compilação JIT. Loops maiores que isso são muito dispendiosos para serem compilados. Quando o loop for executado 16 vezes, o script será compilado em JIT em segundo plano. Quando a compilação JIT for concluída, a execução será transferida para o código compilado.
Evitar chamadas repetidas a uma função
Chamar uma função pode ser uma operação cara. Se você estiver chamando uma função em um loop apertado de execução prolongada, considere mover o loop dentro da função.
Considere os seguintes exemplos:
$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'
}
}
}
O exemplo básico de loop é a linha de base para desempenho. O segundo exemplo encapsula o gerador de números aleatórios em uma função que é chamada em um loop rígido. O terceiro exemplo move o loop dentro da função. A função é chamada apenas uma vez, mas o código ainda gera a mesma quantidade de números aleatórios. Observe a diferença de tempos de execução para cada exemplo.
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
Evitar encapsular pipelines de cmdlet
A maioria dos cmdlets é implementada para o pipeline, que é uma sintaxe sequencial e um processo. Por exemplo:
cmdlet1 | cmdlet2 | cmdlet3
Inicializar um novo pipeline pode ser caro, portanto, você deve evitar encapsular um pipeline de cmdlet em outro pipeline existente.
Considere o exemplo a seguir. O arquivo Input.csv
contém 2100 linhas. O comando Export-Csv
é encapsulado dentro do pipeline ForEach-Object
. O cmdlet Export-Csv
é invocado para cada iteração do loop 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
Para o exemplo a seguir, o comando Export-Csv
foi movido para fora do pipeline ForEach-Object
.
Nesse caso, Export-Csv
é invocado apenas uma vez, mas ainda processa todos os objetos passados de 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
O exemplo desembrulhado é 372 vezes mais rápido. Além disso, observe que a primeira implementação requer o parâmetro Acrescentar, que não é necessário para a implementação posterior.
Usar OrderedDictionary para criar objetos dinamicamente
Há situações em que talvez seja necessário criar objetos dinamicamente com base em alguma entrada, talvez a maneira mais usada para criar um PSObject e, em seguida, adicionar novas propriedades usando o cmdlet Add-Member
. O custo de desempenho para pequenas coleções usando essa técnica pode ser insignificante, no entanto, pode se tornar muito perceptível para grandes coleções. Nesse caso, a abordagem recomendada é usar um [OrderedDictionary]
e convertê-lo em um PSObject usando o acelerador de tipo [pscustomobject]
. Para obter mais informações, consulte a seção Criando dicionários ordenados de about_Hash_Tables.
Suponha que você tenha a seguinte resposta de API armazenada na variável $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" ]
]
}
]
}
Agora, suponha que você queira exportar esses dados para um CSV. Primeiro, você precisa criar objetos e adicionar as propriedades e os valores usando o cmdlet 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
}
Usando um OrderedDictionary
, o código pode ser traduzido para:
$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
}
Em ambos os casos, a saída $result
seria a mesma:
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
Esta última abordagem torna-se exponencialmente mais eficiente à medida que o número de objetos e propriedades de membro aumenta.
Aqui está uma comparação de desempenho de três técnicas para criar objetos com cinco propriedades:
$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'
}
}
}
E estes são os resultados:
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