通过何时使用哪种方法和性能比较的指导来补充现有的有用答案。
对于 small 输入集合(数组),您可能不会注意到差异,而且,尤其是在命令行上,有时能够轻松键入命令更重要。
这是一个易于输入的替代方法,但它是最慢的方法;它使用simplified ForEach-Object syntax called an operation statement(同样,PSv3+):
;例如,以下 PSv3+ 解决方案很容易附加到现有命令:
$objects | % Name # short for: $objects | ForEach-Object -Process { $_.Name }
注意:使用管道并不是这种方法速度慢的主要原因,它是ForEach-Object(和Where-Object)cmdlet 的低效实现 ,至少 PowerShell 7.2。 This excellent blog post 解释问题;它导致了功能请求GitHub issue #10982;以下解决方法大大加快了操作(仅比foreach 语句慢一点,但仍比.ForEach() 快):
# Speed-optimized version of the above.
# (Use `&` instead of `.` to run in a child scope)
$objects | . { process { $_.Name } }
PSv4+ .ForEach() array method(在 this article 中进行了更全面的讨论)是另一种性能良好的替代方案,但请注意它需要收集所有首先在内存中输入,就像成员访问枚举:
# By property name (string):
$objects.ForEach('Name')
# By script block (more flexibility; like ForEach-Object)
$objects.ForEach({ $_.Name })
-
这种方法类似于成员访问枚举,具有相同的权衡,只是不应用管道逻辑;它比成员访问枚举略慢,但仍然明显快于管道。
-
对于通过 name(string 参数)提取单个属性值,此解决方案与成员访问枚举相当(尽管后者在语法上更简单)。
-
script-block 变体 ({ ... }) 允许任意转换;它是一种更快的 - all-in-memory-at-once - 替代基于管道的ForEach-Object cmdlet (%)。
注意:.ForEach() 数组方法,就像它的 .Where() sibling(在内存中等效于 Where-Object),总是返回一个 集合(一个实例[System.Collections.ObjectModel.Collection[psobject]]),即使只生成一个输出对象。
相比之下,成员访问枚举 Select-Object、ForEach-Object 和 Where-Object 按原样返回单个输出对象,而不将其包装在集合(数组)中。
比较各种方法的性能
这里是各种方法的采样时间,基于 10,000 对象的输入集合,在 10 次运行中取平均值;绝对数字并不重要,并且会因许多因素而异,但它应该让您了解相对性能(时间来自单核 Windows 10 VM:
重要
-
相对性能因输入对象是常规 .NET 类型的实例(例如,Get-ChildItem 的输出)还是 [pscustomobject] 实例而有所不同em>(例如,Convert-FromCsv 的输出)。
原因是[pscustomobject] 属性由 PowerShell 动态管理,它可以比(静态定义的)常规 .NET 类型的常规属性更快地访问它们。下面介绍了这两种情况。
-
测试使用已经在内存中的完整集合作为输入,以便专注于纯属性提取性能。使用流式 cmdlet/函数调用作为输入,性能差异通常不会那么明显,因为在调用中花费的时间可能占了大部分时间。
-
为简洁起见,别名 % 用于 ForEach-Object cmdlet。
一般结论,适用于常规 .NET 类型和[pscustomobject] 输入:
-
成员枚举 ($collection.Name) 和 foreach ($obj in $collection) 解决方案是迄今为止最快的,比基于管道的最快解决方案快 10 倍或更多。
-
令人惊讶的是,% Name 的性能比 % { $_.Name } 差得多 - 请参阅 this GitHub issue。
-
PowerShell Core 在这方面始终优于 Windows Powershell。
使用常规 .NET 类型的时间:
- PowerShell Core v7.0.0-preview.3
Factor Command Secs (10-run avg.)
------ ------- ------------------
1.00 $objects.Name 0.005
1.06 foreach($o in $objects) { $o.Name } 0.005
6.25 $objects.ForEach('Name') 0.028
10.22 $objects.ForEach({ $_.Name }) 0.046
17.52 $objects | % { $_.Name } 0.079
30.97 $objects | Select-Object -ExpandProperty Name 0.140
32.76 $objects | % Name 0.148
- Windows PowerShell v5.1.18362.145
Factor Command Secs (10-run avg.)
------ ------- ------------------
1.00 $objects.Name 0.012
1.32 foreach($o in $objects) { $o.Name } 0.015
9.07 $objects.ForEach({ $_.Name }) 0.105
10.30 $objects.ForEach('Name') 0.119
12.70 $objects | % { $_.Name } 0.147
27.04 $objects | % Name 0.312
29.70 $objects | Select-Object -ExpandProperty Name 0.343
结论:
- 在 PowerShell Core 中,
.ForEach('Name') 的性能明显优于 .ForEach({ $_.Name })。奇怪的是,在 Windows PowerShell 中,后者更快,尽管只是稍微快一点。
时间与 [pscustomobject] 实例:
- PowerShell Core v7.0.0-preview.3
Factor Command Secs (10-run avg.)
------ ------- ------------------
1.00 $objects.Name 0.006
1.11 foreach($o in $objects) { $o.Name } 0.007
1.52 $objects.ForEach('Name') 0.009
6.11 $objects.ForEach({ $_.Name }) 0.038
9.47 $objects | Select-Object -ExpandProperty Name 0.058
10.29 $objects | % { $_.Name } 0.063
29.77 $objects | % Name 0.184
- Windows PowerShell v5.1.18362.145
Factor Command Secs (10-run avg.)
------ ------- ------------------
1.00 $objects.Name 0.008
1.14 foreach($o in $objects) { $o.Name } 0.009
1.76 $objects.ForEach('Name') 0.015
10.36 $objects | Select-Object -ExpandProperty Name 0.085
11.18 $objects.ForEach({ $_.Name }) 0.092
16.79 $objects | % { $_.Name } 0.138
61.14 $objects | % Name 0.503
结论:
-
注意[pscustomobject] 输入.ForEach('Name') 的性能远远优于基于脚本块的变体.ForEach({ $_.Name })。
-
同样,[pscustomobject] 输入使基于管道的Select-Object -ExpandProperty Name 更快,在 Windows PowerShell 中几乎与.ForEach({ $_.Name }) 相当,但在 PowerShell Core 中仍然慢约 50%。
-
简而言之:除了% Name 的奇怪例外,[pscustomobject] 引用属性的基于字符串的方法优于基于脚本块的方法。
测试源代码:
注意:
$count = 1e4 # max. input object count == 10,000
$runs = 10 # number of runs to average
# Note: Using [pscustomobject] instances rather than instances of
# regular .NET types changes the performance characteristics.
# Set this to $true to test with [pscustomobject] instances below.
$useCustomObjectInput = $false
# Create sample input objects.
if ($useCustomObjectInput) {
# Use [pscustomobject] instances.
$objects = 1..$count | % { [pscustomobject] @{ Name = "$foobar_$_"; Other1 = 1; Other2 = 2; Other3 = 3; Other4 = 4 } }
} else {
# Use instances of a regular .NET type.
# Note: The actual count of files and folders in your file-system
# may be less than $count
$objects = Get-ChildItem / -Recurse -ErrorAction Ignore | Select-Object -First $count
}
Write-Host "Comparing property-value extraction methods with $($objects.Count) input objects, averaged over $runs runs..."
# An array of script blocks with the various approaches.
$approaches = { $objects | Select-Object -ExpandProperty Name },
{ $objects | % Name },
{ $objects | % { $_.Name } },
{ $objects.ForEach('Name') },
{ $objects.ForEach({ $_.Name }) },
{ $objects.Name },
{ foreach($o in $objects) { $o.Name } }
# Time the approaches and sort them by execution time (fastest first):
Time-Command $approaches -Count $runs | Select Factor, Command, Secs*
[1] 从技术上讲,即使是没有| 的命令pipeline operator,也会在后台使用管道,但出于本次讨论的目的,使用管道仅指对于使用管道运算符| 的命令,因此根据定义涉及多个命令。