PowerShell 脚本性能注意事项

直接利用 .NET 并避免管道的 PowerShell 脚本往往比惯用 PowerShell 更快。 惯用的 PowerShell 使用 cmdlet 和 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'
        }
    }
}

这些测试是在 PowerShell 7.3.4 中的 Windows 11 计算机上运行的。 结果如下所示:

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

使用数组加法的性能影响随集合大小和数字添加量呈指数级增长。 此代码将显式赋值与使用数组加法和对 [List<T>] 对象使用 Add(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'
        }
    }
}

这些测试是在 PowerShell 7.3.4 中的 Windows 11 计算机上运行的。

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[]]

字符串加法

字符串是不可变的。 每次添加字符串实际上都会创建一个新字符串,足以容纳左右操作数的内容,然后将两个操作数的元素复制到新字符串中。 对于小字符串,这种开销可能无关紧要。 对于大型字符串,这可能会影响性能和内存消耗。

至少有两种替代方法:

以下示例比较了生成字符串的这三种方法的性能。

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

这些测试是在 PowerShell 7.4.2 中的 Windows 11 计算机上运行的。 输出显示 -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

这可以比直接使用 .NET API 慢一个数量级。 例如,可以使用 .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()
    }
}

还可以使用包装 StreamReader[System.IO.File]ReadLines 方法简化读取过程:

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

按大型集合中的属性查找条目

通常,需要使用共享属性来标识不同集合中的同一记录,例如使用名称从一个列表中检索 ID,另一个集合中的电子邮件。 循环访问第一个列表以查找第二个集合中的匹配记录速度较慢。 具体而言,第二个集合的重复筛选会产生很大的开销。

给定两个集合,一个具有 ID名称,另一个集合 名称电子邮件

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

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

协调这些集合以返回具有 ID名称的对象列表,以及电子邮件 属性 通常如下所示:

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

但是,该实现必须针对 $Employee 集合中的每个项筛选一次 $Accounts 集合中的所有 5000 项。 这可能需要几分钟时间,即使对于此单值查找也是如此。

相反,你可以创建一个 哈希表,该表使用共享 名称 属性作为键,并将匹配的帐户用作值。

$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

仅当需要将格式化文本写入主机控制台时,才应使用 Write-Host 命令,而不是将对象写入 Success 管道。

对于特定主机(如 pwsh.exepowershell.exepowershell_ise.exe)而言,Write-Host 可能比 [Console]::WriteLine() 慢一个数量级。 但是,不能保证 [Console]::WriteLine() 在所有主机中都正常工作。 此外,使用 [Console]::WriteLine() 编写的输出不会写入 Start-Transcript启动的脚本。

JIT 编译

PowerShell 将脚本代码编译为解释的字节代码。 从 PowerShell 3 开始,对于循环中重复执行的代码,PowerShell 可以通过将代码实时(JIT)编译为本机代码来提高性能。

少于 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'
        }
    }
}

基本 for-loop 示例是性能的基线。 第二个示例将随机数生成器包装在紧密循环中调用的函数中。 第三个示例在函数内移动循环。 该函数只调用一次,但代码仍生成相同的随机数。 请注意每个示例的执行时间差异。

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

避免包装 cmdlet 管道

大多数 cmdlet 都是针对管道实现的,这是一种顺序语法和过程。 例如:

cmdlet1 | cmdlet2 | cmdlet3

初始化新管道可能很昂贵,因此应避免将 cmdlet 管道包装到另一个现有管道中。

请考虑以下示例。 Input.csv 文件包含 2100 行。 Export-Csv 命令包装在 ForEach-Object 管道中。 Export-Csv cmdlet 针对 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 参数,这在以后的实现中不需要。

对象创建

使用 New-Object cmdlet 创建对象可能会很慢。 以下代码将使用 New-Object cmdlet 创建对象的性能与 [pscustomobject] 类型加速器进行比较。

Measure-Command {
    $test = 'PSCustomObject'
    for ($i = 0; $i -lt 100000; $i++) {
        $resultObject = [PSCustomObject]@{
            Name = 'Name'
            Path = 'FullName'
        }
    }
} | Select-Object @{n='Test';e={$test}},TotalSeconds

Measure-Command {
    $test = 'New-Object'
    for ($i = 0; $i -lt 100000; $i++) {
        $resultObject = New-Object -TypeName PSObject -Property @{
            Name = 'Name'
            Path = 'FullName'
        }
    }
} | Select-Object @{n='Test';e={$test}},TotalSeconds
Test           TotalSeconds
----           ------------
PSCustomObject         0.48
New-Object             3.37

PowerShell 5.0 为所有 .NET 类型添加了 new() 静态方法。 以下代码将使用 New-Object cmdlet 创建对象的性能与 new() 方法进行比较。

Measure-Command {
    $test = 'new() method'
    for ($i = 0; $i -lt 100000; $i++) {
        $sb = [System.Text.StringBuilder]::new(1000)
    }
} | Select-Object @{n='Test';e={$test}},TotalSeconds

Measure-Command {
    $test = 'New-Object'
    for ($i = 0; $i -lt 100000; $i++) {
        $sb = New-Object -TypeName System.Text.StringBuilder -ArgumentList 1000
    }
} | Select-Object @{n='Test';e={$test}},TotalSeconds
Test         TotalSeconds
----         ------------
new() method         0.59
New-Object           3.17

使用 OrderedDictionary 动态创建新对象

在某些情况下,我们可能需要根据某些输入动态创建对象,这也许是最常用的方法来创建新的 PSObject,然后使用 Add-Member cmdlet 添加新属性。 使用此技术的小集合的性能成本可能微不足道,但对于大型集合来说,它可能会变得非常明显。 在这种情况下,建议的方法是使用 [OrderedDictionary],然后使用 [pscustomobject] 类型加速器将其转换为 PSObject。 有关详细信息,请参阅 创建有序字典 部分 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 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
}

使用 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