Búsqueda de sitios web

Consideraciones sobre el rendimiento de las secuencias de comandos de PowerShell


Los scripts de PowerShell que aprovechan .NET directamente y evitan la canalización tienden a ser más rápidos que los PowerShell idiomáticos. Idiomatic PowerShell utiliza cmdlets y funciones de PowerShell, a menudo aprovechando la canalización y recurriendo a .NET solo cuando es necesario.

Nota

Muchas de las técnicas descritas aquí no son idiomáticas de PowerShell y pueden reducir la legibilidad de un script de PowerShell. Se recomienda a los autores de scripts que utilicen PowerShell idiomático a menos que el rendimiento indique lo contrario.

Suprimir la salida

Hay muchas formas de evitar escribir objetos en la canalización.

  • Asignación o redirección de archivos a $null
  • Transmitiendo a [void]
  • Tubería a Out-Null

Las velocidades de asignación a $null, conversión a [void] y redirección de archivos a $null son casi idénticas. Sin embargo, llamar a Out-Null en un bucle grande puede ser significativamente más lento, especialmente en 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'
        }
    }
}

Estas pruebas se ejecutaron en una máquina con Windows 11 en PowerShell 7.3.4. Los resultados se muestran a continuación:

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

Los tiempos y las velocidades relativas pueden variar según el hardware, la versión de PowerShell y la carga de trabajo actual del sistema.

adición de matriz

La generación de una lista de elementos a menudo se realiza utilizando una matriz con el operador de suma:

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

La adición de matrices es ineficiente porque las matrices tienen un tamaño fijo. Cada adición a la matriz crea una nueva matriz lo suficientemente grande como para contener todos los elementos de los operandos izquierdo y derecho. Los elementos de ambos operandos se copian en la nueva matriz. Para colecciones pequeñas, es posible que esta sobrecarga no importe. El rendimiento puede verse afectado por colecciones grandes.

Hay un par de alternativas. Si en realidad no necesita una matriz, considere usar una lista genérica escrita ([List]):

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

El impacto en el rendimiento del uso de la suma de matrices crece exponencialmente con el tamaño de la colección y las sumas de números. Este código compara la asignación explícita de valores a una matriz con el uso de la suma de matrices y el uso del método Add(T) en un objeto [List]. Define la asignación explícita como base para el desempeño.

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

Estas pruebas se ejecutaron en una máquina con Windows 11 en 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

Cuando se trabaja con colecciones grandes, la adición de matrices es dramáticamente más lenta que la adición de

List<T>

Cuando se utiliza un objeto [List], es necesario crear la lista con un tipo específico, como [String] o [Int]. Cuando agrega objetos de un tipo diferente a la lista, se convierten al tipo especificado. Si no se pueden convertir al tipo especificado, el método genera una excepción.

$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

Cuando necesite que la lista sea una colección de diferentes tipos de objetos, créela con [Object] como tipo de lista. Puede enumerar la colección e inspeccionar los tipos de objetos que contiene.

$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

Si necesita una matriz, puede llamar al método ToArray() en la lista o puede dejar que PowerShell cree la matriz por usted:

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

En este ejemplo, PowerShell crea un [ArrayList] para contener los resultados escritos en la canalización dentro de la expresión de matriz. Justo antes de asignar a $results, PowerShell convierte [ArrayList] en un [Object[]].

Adición de cadenas

Las cadenas son inmutables. Cada adición a la cadena en realidad crea una nueva cadena lo suficientemente grande como para contener el contenido de los operandos izquierdo y derecho, luego copia los elementos de ambos operandos en la nueva cadena. Para cadenas pequeñas, esta sobrecarga puede no importar. Para cadenas grandes, esto puede afectar el rendimiento y el consumo de memoria.

Hay al menos dos alternativas:

  • El operador -join concatena cadenas
  • La clase .NET [StringBuilder] proporciona una cadena mutable

El siguiente ejemplo compara el rendimiento de estos tres métodos de construcción de una cadena.

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

Estas pruebas se ejecutaron en una máquina con Windows 11 en PowerShell 7.4.2. El resultado muestra que el operador -join es el más rápido, seguido de la clase [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

Los tiempos y las velocidades relativas pueden variar según el hardware, la versión de PowerShell y la carga de trabajo actual del sistema.

Procesamiento de archivos grandes

La forma idiomática de procesar un archivo en PowerShell podría verse así:

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

Esto puede ser un orden de magnitud más lento que usar las API de .NET directamente. Por ejemplo, puede utilizar la clase .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()
    }
}

También puede utilizar el método ReadLines de [System.IO.File], que envuelve StreamReader y simplifica el proceso de lectura:

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

Buscar entradas por propiedad en grandes colecciones

Es común necesitar usar una propiedad compartida para identificar el mismo registro en diferentes colecciones, como usar un nombre para recuperar una identificación de una lista y un correo electrónico de otra. Iterar sobre la primera lista para encontrar el registro coincidente en la segunda colección es lento. En particular, el filtrado repetido de la segunda colección tiene una gran sobrecarga.

Dadas dos colecciones, una con un ID y un Nombre, la otra con un Nombre y un Correo electrónico:

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

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

La forma habitual de conciliar estas colecciones para devolver una lista de objetos con las propiedades ID, Nombre y Email podría verse así:

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

Sin embargo, esa implementación tiene que filtrar los 5000 elementos de la colección $Accounts una vez para cada elemento de la colección $Employee. Esto puede llevar unos minutos, incluso para esta búsqueda de un solo valor.

En su lugar, puede crear una tabla hash que utilice la propiedad Nombre compartida como clave y la cuenta coincidente como valor.

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

Buscar claves en una tabla hash es mucho más rápido que filtrar una colección por valores de propiedad. En lugar de verificar cada elemento de la colección, PowerShell puede verificar si la clave está definida y usar su valor.

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

Esto es mucho más rápido. Mientras que el filtro de bucle tardó unos minutos en completarse, la búsqueda de hash tarda menos de un segundo.

Utilice Write-Host con cuidado

El comando Write-Host solo debe usarse cuando necesite escribir texto formateado en la consola del host, en lugar de escribir objetos en la canalización Success.

Write-Host puede ser un orden de magnitud más lento que [Console]::WriteLine() para hosts específicos como pwsh.exe, powershell.exe o powershell_ise.exe. Sin embargo, no se garantiza que [Console]::WriteLine() funcione en todos los hosts. Además, la salida escrita usando [Console]::WriteLine() no se escribe en transcripciones iniciadas por Start-Transcript.

Compilación JIT

PowerShell compila el código del script en un código de bytes que se interpreta. A partir de PowerShell 3, para el código que se ejecuta repetidamente en un bucle, PowerShell puede mejorar el rendimiento compilando el código justo a tiempo (JIT) en código nativo.

Los bucles que tienen menos de 300 instrucciones son elegibles para la compilación JIT. Los bucles más grandes que eso son demasiado costosos de compilar. Cuando el bucle se ha ejecutado 16 veces, el script se compila JIT en segundo plano. Cuando se completa la compilación JIT, la ejecución se transfiere al código compilado.

Evite llamadas repetidas a una función

Llamar a una función puede ser una operación costosa. Si está llamando a una función en un bucle cerrado de larga duración, considere mover el bucle dentro de la función.

Considere los siguientes ejemplos:

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

El ejemplo del bucle for básico es la base para el rendimiento. El segundo ejemplo envuelve el generador de números aleatorios en una función que se llama en un bucle cerrado. El tercer ejemplo mueve el bucle dentro de la función. La función solo se llama una vez pero el código aún genera la misma cantidad de números aleatorios. Observe la diferencia en los tiempos de ejecución para cada ejemplo.

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

Evite envolver canalizaciones de cmdlet

La mayoría de los cmdlets se implementan para la canalización, que es un proceso y una sintaxis secuencial. Por ejemplo:

cmdlet1 | cmdlet2 | cmdlet3

Inicializar una nueva canalización puede resultar costoso; por lo tanto, debe evitar incluir una canalización de cmdlet en otra canalización existente.

Considere el siguiente ejemplo. El archivo Input.csv contiene 2100 líneas. El comando Export-Csv está incluido dentro de la canalización ForEach-Object. El cmdlet Export-Csv se invoca para cada iteración del bucle 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 el siguiente ejemplo, el comando Export-Csv se movió fuera de la canalización ForEach-Object. En este caso, Export-Csv se invoca solo una vez, pero aún procesa todos los objetos pasados desde 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

El ejemplo desenvuelto es 372 veces más rápido. Además, observe que la primera implementación requiere el parámetro Append, que no es necesario para la implementación posterior.

Utilice OrderedDictionary para crear dinámicamente nuevos objetos

Hay situaciones en las que es posible que necesitemos crear objetos dinámicamente en función de alguna entrada, quizás la forma más comúnmente utilizada de crear un nuevo PSObject y luego agregar nuevas propiedades usando el Add-Member cmdlet. El costo de rendimiento para colecciones pequeñas que utilizan esta técnica puede ser insignificante; sin embargo, puede llegar a ser muy notable para colecciones grandes. En ese caso, el enfoque recomendado es utilizar un [OrderedDictionary] y luego convertirlo en un PSObject usando el acelerador de tipo [pscustomobject]. Para obtener más información, consulte la sección Creación de diccionarios ordenados de about_Hash_Tables.

Supongamos que tiene la siguiente respuesta API almacenada en la variable $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" ]
      ]
    }
  ]
}

Ahora, supongamos que desea exportar estos datos a un CSV. Primero debe crear nuevos objetos y agregar las propiedades y valores usando el 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 un OrderedDictionary, el código se puede traducir a:

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

En ambos casos, la salida $result sería la misma:

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

El último enfoque se vuelve exponencialmente más eficiente a medida que aumenta el número de objetos y propiedades de los miembros.

Aquí hay una comparación de rendimiento de tres técnicas para crear objetos con 5 propiedades:

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

Y estos son los 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

Artículos relacionados