「原本几分钟就能跑完的汇总脚本,随着数据增加变成了要跑三个小时」── 这是在 PowerShell 自动化已经普及的现场经常听到的说法。而在大多数情况下,变慢的原因不在于 PowerShell 本身的执行速度,而在于写法。
PowerShell 是一门以易写性为优先的语言,因此存在不少「按照自然的写法写出来,结果计算量却恶化了」的模式。最具代表性的就是 $array += $item。看起来很自然,但内部每次都会执行整个数组的复制,随着件数增加会急剧变慢。是否了解这些常见的陷阱,决定了同一处理是需要几个小时,还是只需要几十秒。
本文将按影响大小的顺序,整理在看到运行缓慢的脚本时首先应该怀疑的地方。同时也会讲解正确的测量方法,以避免凭主观臆断进行改写。
本文适用的环境同时包括 Windows PowerShell 5.1 与 PowerShell 7。两者行为不同的地方(例如 Get-Content 的默认字符编码等),会在正文中逐处说明。本文的示例代码是在 PowerShell 7.6 中执行验证的。
1. 先说结论
- 请不要使用
$array += $item。PowerShell 的数组是固定长度的,+=每次都会创建新数组并复制全部元素。速度会按件数的平方比例变慢。1 - 请改用
List[T],或者用变量接收循环整体的输出。后者是既符合 PowerShell 风格、又更快的写法。 - 匹配・检索最有效的手段是哈希表化。双重循环的线性搜索(O(n×m))会变为按键引用(大致为 O(n+m))。2
- 管道虽然方便,但每个对象都有相应的开销。在处理大量件数的内层循环中,
foreach语句往往更快,值得实测比较。3 - 文件读取要根据目的分别使用。用
Get-Content -Raw一次性读取,用-ReadCount分批读取。有些情况下逐行对象化会是瓶颈所在。4 Get-ChildItem请使用-Filter。官方文档也明确指出,由于是在提供程序一侧进行筛选,比获取后再用Where-Object丢弃更高效。5- 字符串的拼接请使用
-join或StringBuilder。$s += "..."与数组变慢的原因相同。6 Format-*只在屏幕显示的最后使用。如果在中途插入,不仅会破坏后续处理,还会产生无谓的格式化开销。7- 先测量,再修改。
Measure-Command会丢弃输出,因此不包含显示开销。不要使用首次执行的数值。8 - 并行化放在最后。请先修正算法,再考虑并行化(参见「PowerShell 的并行处理」)。
2. 先测量 ── Measure-Command 的正确用法
凭猜测进行调优,大多会修改到没有效果的地方。首先要做的是测量。8
先做一个说明。本文代码示例中出现的 Invoke-KsAggregate、ConvertTo-KsRecord、Join-KsRecord,都是供读者替换为自己处理逻辑的临时函数名。它们并非 PowerShell 的标准 cmdlet,因此直接粘贴运行会因「无法识别该术语」而报错停止。请将其替换为你自己脚本中的函数名或处理逻辑来阅读。
# 第 1 次会包含模块加载与 JIT 编译的时间,因此舍弃
$null = Measure-Command { Invoke-KsAggregate }
# 第 2 次及以后测量数次,同时观察波动情况
1..3 | ForEach-Object {
(Measure-Command { Invoke-KsAggregate }).TotalSeconds
}
Measure-Command 有两点需要注意。
- 脚本块的输出会被丢弃。不包含屏幕上的格式化・渲染开销。如果实际体感很慢,有时元凶其实在显示这一侧
- 要在相同条件下比较。文件缓存是否已经预热,会让包含 I/O 的处理的测量值产生很大差异
想要细分处理内部哪个环节慢时,请使用 Stopwatch。
$sw = [System.Diagnostics.Stopwatch]::StartNew()
$rows = Import-Csv $csvPath
Write-Verbose "读取: $($sw.ElapsedMilliseconds) ms"; $sw.Restart()
$index = $master | Group-Object Code -AsHashTable -AsString
Write-Verbose "创建索引: $($sw.ElapsedMilliseconds) ms"; $sw.Restart()
$result = foreach ($r in $rows) { Join-KsRecord $r $index }
Write-Verbose "匹配: $($sw.ElapsedMilliseconds) ms"; $sw.Stop()
像这样输出各区间的耗时,就能立刻发现「原来读取占了 8 成」这样的事实。之所以使用 Write-Verbose,是为了让它在正常运行时不显示,只在排查时才可见(参见「PowerShell 的输出流与日志设计」)。
3. 最大的元凶 ── 数组的 +=
PowerShell 的数组是固定长度的。无法直接追加元素,+= 会被展开为「创建新数组、复制全部元素、在末尾追加新元素」的处理。1 也就是说,追加 n 件的循环整体上会执行与 n 的平方成比例的复制。
具体会增加多少,可以直接从写法算出来。追加 n 件时发生的元素复制总次数为 n(n-1)/2。
| 追加的件数 | += 产生的元素复制总次数 |
List[T].Add 的追加次数 |
|---|---|---|
| 1,000 件 | 约 50 万次 | 1,000 次 |
| 10,000 件 | 约 5,000 万次 | 10,000 次 |
| 100,000 件 | 约 50 亿次 | 100,000 次 |
这并非执行时间的实测值,而是由写法唯一确定的处理次数。每一件的实际耗时会因环境而异,因此自己环境下的秒数,请用配套 zip 中的基准测试来测量。不过,「件数增加 10 倍时次数会增加到 100 倍」这种增长方式,与环境无关,始终相同。
# 【NG】件数增加时会急剧变慢
$result = @()
foreach ($row in $rows) {
$result += ConvertTo-KsRecord $row # 每次都会复制整个数组
}
# 【OK-1】使用 List[T](元素追加是常数时间)
$result = [System.Collections.Generic.List[object]]::new()
foreach ($row in $rows) {
$result.Add((ConvertTo-KsRecord $row))
}
# 【OK-2】用变量接收循环整体的输出(符合 PowerShell 风格,且更快)
$result = foreach ($row in $rows) {
ConvertTo-KsRecord $row # 输出会被直接汇总
}
【OK-2】的写法,一旦习惯之后,是 PowerShell 中最自然的形式。foreach 语句、ForEach-Object、if 等的输出都可以直接汇总到变量中。这一特性需要与对「输出流」的理解配套掌握。
同样的道理也适用于字符串。字符串是不可变的(immutable),因此 $s += "line" 每次都会创建新字符串。
# 【NG】
$text = ''
foreach ($line in $lines) { $text += "$line`r`n" }
# 【OK-1】-join(最简洁)
$text = $lines -join "`r`n"
# 【OK-2】StringBuilder(适用于涉及条件分支的复杂拼装)
$sb = [System.Text.StringBuilder]::new()
foreach ($line in $lines) { [void]$sb.AppendLine($line) }
$text = $sb.ToString()
4. 停止使用双重循环 ── 匹配请用哈希表
接下来有效的是匹配(matching)。「针对订单数据的每一行,从主数据中查出商品名称」这一处理,如果按自然写法来写,会是这样。
# 【NG】订单 1 万件 × 主数据 1 万件 = 1 亿次比较
foreach ($order in $orders) {
$item = $master | Where-Object { $_.Code -eq $order.Code } # 每次都全量扫描
$order | Add-Member NoteProperty ItemName $item.Name
}
把这个改成哈希表后,比较次数会大幅减少。2
# 【OK】只创建一次索引,之后按键引用
$index = $master | Group-Object -Property Code -AsHashTable -AsString
$result = foreach ($order in $orders) {
$hit = $index[$order.Code] # 按键引用不依赖件数
[pscustomobject]@{
Code = $order.Code
Qty = $order.Qty
ItemName = if ($hit) { $hit[0].Name } else { $null } # 也能检测出未登记的情况
}
}
这里同样看一下次数。双重循环的比较次数是订单件数 × 主数据件数,而哈希表是「创建一次索引」+「逐件查询」,因此与合计件数成比例。
| 订单件数 × 主数据件数 | 双重循环的比较次数 | 哈希表的处理次数 |
|---|---|---|
| 1,000 × 1,000 | 100 万次 | 约 2,000 次 |
| 10,000 × 10,000 | 1 亿次 | 约 2 万次 |
| 100,000 × 100,000 | 100 亿次 | 约 20 万次 |
件数增加到 10 倍时,双重循环会变成 100 倍,哈希表则是 10 倍。件数越多,差距就越大,因此对于预计数据量会增加的处理,越值得优先修改。
Group-Object -AsHashTable 会把相同键的元素汇总为数组,因此值会是数组(对应上面的 $hit[0])。如果已经知道键是唯一的,也可以自己组装哈希表。
$index = @{}
foreach ($m in $master) { $index[$m.Code] = $m } # 以键唯一为前提
如果只是反复判定「是否包含」,HashSet 会很有效。针对数组的 -contains 或 -in 是线性搜索,判定次数一多就会成为瓶颈。
$known = [System.Collections.Generic.HashSet[string]]::new(
[string[]]$master.Code, [System.StringComparer]::OrdinalIgnoreCase)
$unknown = $orders | Where-Object { -not $known.Contains($_.Code) }
CSV 之间匹配的实务模式,在「用 PowerShell 自动化 Excel・CSV 业务处理」中也有讲解。
5. 管道与 foreach 语句
管道是 PowerShell 的核心功能,但每个对象在 cmdlet 之间流转都有相应的处理开销。在数十万件规模的内层循环中,foreach 语句往往更快。3
# 管道: 易读,且逐条处理,不会积压中间结果
$rows | Where-Object { $_.Status -eq 'OK' } | ForEach-Object { $_.Amount } |
Measure-Object -Sum
# foreach 语句: 每件的开销较小,大量件数时更快
$sum = 0
foreach ($r in $rows) { if ($r.Status -eq 'OK') { $sum += $r.Amount } }
关于内存有一点容易被误解,这里补充说明。foreach 语句会先对圆括号内的表达式求值,然后才进入循环,因此像 foreach ($line in (Get-Content $path)) 这样传入命令的执行结果时,此时全部内容就已经加载到内存中了。3 另一方面,如果传入的是延迟枚举的 IEnumerable,则会逐条枚举。因此即使是巨大文件,只要按下面的写法书写,就能在保持 foreach 语句形式的同时不占用内存来处理。
# 不把全部内容加载到内存,用 foreach 语句处理(延迟枚举)
foreach ($line in [System.IO.File]::ReadLines($path)) {
if ($line.StartsWith('ERROR')) { $errors++ }
}
不过请注意,默认字符编码是不同的这一点。只传一个参数的 ReadLines($path) 会按 UTF-8 读取(如果有 BOM 则优先按 BOM)。而 Windows PowerShell 5.1 的 Get-Content,在没有 BOM 时会按当前的 ANSI 代码页(在日语环境下为 Shift_JIS)读取。也就是说,如果直接替换 Shift_JIS 的日志,日文会出现乱码。替换时请使用能够明确指定编码的重载。
# 读取 Shift_JIS(CP932)日志的情况
# PowerShell 6.2 及以后已经注册了代码页提供程序
# 因此可以直接调用 GetEncoding(932)(Windows PowerShell 5.1 中同样如此)
$enc = [System.Text.Encoding]::GetEncoding(932)
foreach ($line in [System.IO.File]::ReadLines($path, $enc)) {
if ($line.StartsWith('ERROR')) { $errors++ }
}
另外,PowerShell 7 中 Get-Content 的默认编码是 UTF-8(无 BOM),因此只要在 7 中处理的是 UTF-8 文件,即便使用单参数的重载,结果也是一致的。替换之前,请务必确认目标文件的字符编码以及运行环境的版本。
判断的大致标准如下。
| 情况 | 选择 |
|---|---|
| 件数较少・优先可读性 | 管道 |
传入命令结果的巨大输入(Get-Content 等) |
管道,或 [IO.File]::ReadLines + foreach 语句 |
| 对已经在内存中的数组循环数十万次 | foreach 语句 |
| 对数组的简单筛选 | .Where({...}) / .ForEach({...}) 方法1 |
.Where() 和 .ForEach() 是 PowerShell 4.0 中新增的数组方法,由于不经过管道,所以更轻量。1 不过前提是对象已经是内存中的集合。
6. 文件与目录的 I/O
读取要根据目的分别选用。Get-Content 默认会逐行生成对象,因此对巨大文件而言,这项开销会成为主导因素。4
Get-Content $path # 逐行(生成与行数相同数量的对象)
Get-Content $path -Raw # 把整体作为一个字符串一次性读取
Get-Content $path -ReadCount 1000 # 以每 1000 行为一个数组传出(减少对象生成)
[System.IO.File]::ReadLines($path) # .NET。逐条枚举,最轻量(默认为 UTF-8)
目录遍历请使用 -Filter。官方文档明确指出「筛选器是由提供程序在获取对象时应用的,因此比其他参数更高效」。5
# 【NG】先获取全部,再丢弃
Get-ChildItem -Path $root -Recurse | Where-Object { $_.Extension -eq '.log' }
# 【OK】在提供程序一侧筛选
Get-ChildItem -Path $root -Recurse -File -Filter '*.log'
# 在数十万文件规模下如果仍然很慢,可以考虑使用 .NET 的枚举
[System.IO.Directory]::EnumerateFiles($root, '*.log', 'AllDirectories')
写入方面,循环内每次追加写入是典型的瓶颈。Add-Content 每次调用都会打开并关闭文件。
# 【NG】1 万行 = 1 万次打开/关闭
foreach ($r in $result) { Add-Content -Path $out -Value ($r -join ',') }
# 【OK-1】一次性汇总写入
$result | ForEach-Object { $_ -join ',' } | Set-Content -Path $out -Encoding utf8
# 【OK-2】如果需要逐次写出,让 StreamWriter 保持打开状态
$writer = [System.IO.StreamWriter]::new($out, $false, [System.Text.UTF8Encoding]::new($false))
try { foreach ($r in $result) { $writer.WriteLine($r -join ',') } }
finally { $writer.Dispose() }
在跨网络的文件操作中,往返次数本身就会成为主导因素。UNC 路径特有的注意事项,请参见「网络共享・UNC 路径的陷阱」。
7. 容易被忽视的小成本
为了在纠结该从哪里下手时有个参考,下面附上效果大小的大致标准。这不是实测值,而是由「单次成本 × 执行次数」决定的数量级感觉。请对照自己脚本的件数来阅读。
Write-Progress的更新频率。如果每次循环都更新进度,绘制开销有时会超过处理本身的耗时。可以每 100 件等间隔更新一次,或在无人值守执行时用$ProgressPreference = 'SilentlyContinue'关闭 ── 效果程度:大。每次绘制的开销较重,循环次数会直接影响结果。数万次以上的循环应优先检查这一点- 在中途插入
Format-Table/Format-List。由于会被转换成用于格式化显示的对象,不仅会破坏后续处理,还是一项无谓的开销。显示应仅放在最后7 ── 效果程度:中。比起速度,「破坏后续处理」造成的实际损害更大,即便不是出于速度目的也值得修正 - 循环内的不变处理。只需把
Get-Date的格式字符串生成、正则表达式的编译、模块的重新加载等移到循环外部,有时就会很有效果 ── 效果程度:中~大。取决于被移出的那项处理单次的成本,如果混入了像模块重新加载这样开销较大的处理,效果会非常显著 - 大量使用
Select-Object -Property。由于会生成新的 PSCustomObject,在件数庞大时不可忽视。有时保留原始对象直到确实需要的阶段会更快 ── 效果程度:小~中。在数千件规模下感受不到,超过数万件之后开始起作用 - 异常频发。大量依赖
try/catch的设计(例如每次都对不存在的文件执行Get-Item并捕获异常)开销很高。请预先用Test-Path进行分支判断 ── 效果程度:大。异常的抛出与捕获远比普通的分支判断沉重,件数 × 异常发生率越大,影响就越占主导
8. 实务定式(判断表)
| 症状 | 怀疑之处 | 对策 | 详见 |
|---|---|---|---|
| 件数增加后突然变慢 | $array += / $string += |
List[T]、用变量接收循环输出、-join1 |
第 3 章 |
| 两份数据的匹配迟迟无法完成 | 双重循环的线性搜索 | 哈希表 / Group-Object -AsHashTable2 |
第 4 章 |
| 巨大文件的读取很慢 | Get-Content 的逐行对象生成 |
-Raw / -ReadCount / [File]::ReadLines4 |
第 6 章(字符编码的注意事项见第 5 章) |
| 文件夹遍历很慢 | 用 Where-Object 进行后段筛选 |
Get-ChildItem -Filter,必要时用 EnumerateFiles5 |
第 6 章 |
| 输出文件的写入很慢 | 循环内的 Add-Content |
一次性写入,或使用 StreamWriter6 |
第 6 章 |
| CPU 空闲却迟迟不结束 | 网络・I/O 等待 | 该轮到并行化了 | 另一篇文章「并行处理」 |
| 屏幕显示很慢 | 格式化处理・绘制 | Format-* 只放在最后,禁用 $ProgressPreference7 |
第 7 章 |
| 不知道哪里慢 | 测量不足 | 用 Stopwatch 分区间计时,Measure-Command 取第 2 次及以后的值8 |
第 2 章 |
9. 总结
- 首先要测量。请注意
Measure-Command会丢弃输出、不包含显示开销,以及首次执行的数值不可用这两点。 $array +=会按件数的平方比例变慢。改用List[T]或用变量接收循环输出,是最优先要做的事。- 匹配的哈希表化是费效比最高的改进。发现双重循环时,请考虑能否建立索引。
- 文件 I/O 要根据目的选择读写方式。通过
-Filter在提供程序一侧筛选、一次性写入、StreamWriter的使用场景划分,是基本要点。 - 不在中途插入
Format-*、间隔更新进度、把循环内的不变处理移出去 ── 这些小的积累在件数多时也会起作用。 - 并行化是最后的手段。请先修正算法,再将其应用于以等待时间为主导的处理。
示例代码下载
本文中涉及的代码,已整理成可以直接运行的形式发布。其中包含 3 种基准测试和计测辅助工具。
本文的示例,已经在 PowerShell 7.6 中实际执行验证(Pester 8 项)。运行 zip 中包含的 Invoke-SampleTests.ps1,你也可以在自己的环境中重现同样的验证。
# 语法解析 + 静态分析 + Pester 测试
./Invoke-SampleTests.ps1
配置值(路径、服务器名、租户 ID 等)均为示例。请勿直接在生产环境中运行,请根据贵公司的环境自行替换后再使用。
相关文章
- PowerShell 的并行处理 ── ForEach-Object -Parallel 与作业(Job)的选用之道
- 不再使用 Write-Host ── PowerShell 的输出流与日志设计
- 用 PowerShell 自动化 Excel・CSV 业务处理 ── 汇总・核对・报表输出的实务方案
- PowerShell 实用命令合集 —— 积累日常工作中常用的小工具
- 在 Windows 上正确比较不同版本程序运行速度的方法
- 用 PerfView 与 dotnet-trace 定位“变慢”的原因 ── .NET 性能排查实务入门
相关咨询领域
合同会社小村软件提供耗时过长的汇总・匹配处理的提速、能够承受数据量增长的处理架构改造,以及性能劣化原因调查等服务。
参考链接
-
Microsoft Learn,about_Arrays。关于 PowerShell 的数组是固定大小的,+= 运算符的行为是创建新数组并复制现有全部元素后再追加新元素,反复进行元素的追加・删除时适合使用集合类型(如 List)等内容,以及 PowerShell 4.0 中新增的 .Where() 和 .ForEach() 方法。 ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn,Group-Object。关于通过 -AsHashTable 可以把分组结果以哈希表形式返回,通过 -AsString 可以把键作为字符串处理,以及返回的哈希表中每个值都是各分组元素组成的数组。 ↩ ↩2 ↩3
-
Microsoft Learn,about_Foreach。关于 foreach 语句会先对圆括号内的表达式求值再开始迭代,因此传入命令的执行结果时,其结果会被整体保留在内存中,而 ForEach-Object cmdlet 则是从管道逐条接收输入,以及两者的使用场景划分。作为延迟枚举的示例,File.ReadLines 方法说明了无需读取整个文件即可逐行枚举(与 ReadAllLines 的区别)。 ↩ ↩2 ↩3
-
Microsoft Learn,Get-Content。关于默认情况下会按换行符逐行地以对象形式返回,通过 -Raw 可以把整个文件作为一个字符串一次性读取,通过 -ReadCount 可以按指定行数汇总后送入管道等内容。 ↩ ↩2 ↩3
-
Microsoft Learn,Get-ChildItem。关于 -Filter 是由提供程序在获取对象时应用的,因此比获取后再筛选的其他参数更高效,以及与 -File、-Recurse 组合使用等内容。 ↩ ↩2 ↩3
-
Microsoft Learn,StringBuilder 类。关于 String 是不可变的,因此每次连接都会生成新实例,而 StringBuilder 则是在可变的缓冲区上组装字符串。同时说明了StreamWriter 类在保持流打开的状态下逐次写入的行为。 ↩ ↩2
-
Microsoft Learn,Format-Table。关于 Format 系列 cmdlet 会生成用于显示的格式化对象,因此不适合再通过管道传给后续命令,应当放在管道的最后使用。 ↩ ↩2 ↩3
-
Microsoft Learn,Measure-Command。关于会测量脚本块或命令的执行时间并返回 TimeSpan,测量对象本身的输出不会包含在结果中等内容。同时说明了通过Stopwatch 类进行区间计时。 ↩ ↩2 ↩3
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
PowerShell 的并行处理 ── ForEach-Object -Parallel 与作业(Job)的选用之道
本文从实务角度整理 ForEach-Object -Parallel、Start-ThreadJob、Start-Job 的区别与选用之道,$using: 与线程安全性,ThrottleLimit 的确定方法,以及反而会变慢的情形。
不再使用 Write-Host ── PowerShell 的输出流与日志设计
本文整理 PowerShell 六个输出流的用法区分、Write-Host 存在的问题与正确的使用场景、函数返回值被污染的原因、通过 -Verbose 与 -InformationVariable 实现调用方控制,以及结构化日志的留存方法。
从 PowerShell 正确调用外部 exe ── 参数引用、退出代码与乱码的陷阱
从 PowerShell 调用 robocopy 或公司内部 EXE 时,参数会损坏、拿不到退出代码、输出还会出现乱码。本文从实务角度整理 PowerShell 7.3 的参数传递变更、停止解析令牌 --%、以及 Start-Process 的使用区分。
PowerShell 中凭据的安全处理方式 ── 把明文密码逐出脚本
本文整理将 PowerShell 脚本中的明文密码迁移到安全存储的步骤。讲解 SecureString 的真实面貌与局限、Export-Clixml 基于 DPAPI 保存的原理,直至 SecretManagement/SecretStore 的适用场景。
PowerShell 脚本的参数设计与模块化 ── 从「能运行的脚本」到「可以交给别人的脚本」
本文整理把 PowerShell 脚本提升到可以交给他人使用的品质的具体步骤,讲解 param 块与 [CmdletBinding()]、输入验证、管道输入、-WhatIf 支持、.psm1 模块化,直至内部共享与 Git 管理的要点。
相关主题
与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。
Windows 技术主题
汇整 KomuraSoft LLC 关于 Windows 开发、故障调查与既有资产活用文章的主题中心。
常见问题
汇总了咨询这一主题时常见的问题。
- 为什么 `$array += $item` 这种写法会很慢?
- 这是因为 PowerShell 的数组是固定长度的,无法追加元素。`+=` 运算符会被展开为「创建新数组、复制现有全部元素、在末尾追加」这样的处理。每追加一件就要执行一次全量复制,因此追加 n 件时计算量会按 n 的平方比例增长。100 件的话感觉不出来,但超过 1 万件就会慢到能明显感觉出来,10 万件的话就无法实用了。解决办法是改用 System.Collections.Generic.List[T] 的 Add,或者改成用变量接收循环整体输出的写法。
- 用 Measure-Command 测量出的结果,和实际的执行时间对不上。
- 这是因为 Measure-Command 只测量脚本块本身的执行时间,其输出会被丢弃。而在实际执行中,还会加上把结果整理并显示到屏幕上的开销(格式化处理),以及在控制台上的绘制时间。另外,首次执行中包含模块加载与 JIT 编译的时间,因此第一次的测量值大多不可信。请遵守两点:对同一处理执行数次并查看第 2 次以后的数值,比较对象要在相同条件下测量。
- 两个 CSV 的匹配迟迟无法完成,应该改哪里?
- 很可能是内层循环中每次都在用 Where-Object 进行线性搜索。如果两边各有 1 万件,比较次数就会达到 1 亿次。把其中一方转换成哈希表(或用 Group-Object -AsHashTable)改为按键引用的形式,比较次数就能降到与合计件数大致成比例的程度。件数越多效果越大,是费效比最高的改进。
- 听说直接调用 .NET 的 API 会比 cmdlet 更快,是否应该总是这样做?
- 不,这需要区分使用场景。.NET 的 API(例如 System.IO.File 的 ReadLines 或 EnumerateFiles)确实速度更快,但会失去 PowerShell 的提供程序功能、通配符、相对路径解析等便利性,代码也会变得难以阅读。请先测量,只替换实际确认为瓶颈的地方。把使用范围限定在数十万文件的遍历、巨大文件的读取等明确有效的场景中,才是实务上的做法。
- 把处理并行化就能变快吗?
- 如果是以等待时间为主导的处理,并行化会有效果,但按顺序来说它应该放在最后。请先做减少无谓处理的工作(缩小获取件数、把不变的处理从循环中移出去、把匹配哈希表化)。如果算法本身仍是 O(n^2),即使并行化,最多也只能按核心数的倍数变快。反过来,如果对单件很轻量的处理进行并行化,有时反而会因为开销而变慢。