Skip to content

Commit 6cb04e2

Browse files
committed
docs(cheatsheet): 添加pwsh高性能脚本编写最佳实践指南
1 parent 46abe16 commit 6cb04e2

1 file changed

Lines changed: 225 additions & 0 deletions

File tree

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
编写高性能的 PowerShell (pwsh) 脚本不仅仅是让代码“能跑”,而是要理解 PowerShell 的对象模型、管道机制以及底层的 .NET 框架。
2+
3+
以下是编写高性能 PowerShell 脚本的核心最佳实践,按重要性排序:
4+
5+
### 1. 避免在数组中使用 `+=` (最常见的性能杀手)
6+
7+
在 PowerShell 中,数组是固定大小的。每次使用 `+=` 向数组添加元素时,PowerShell 实际上会创建一个**新的、更大的数组**,将旧数据复制过去,加上新数据,然后销毁旧数组。随着数据量增加,这会变成 O(n²) 的操作,速度极慢。
8+
9+
**错误做法:**
10+
11+
```powershell
12+
$results = @()
13+
foreach ($item in $list) {
14+
$results += $item # 随着 $list 变大,速度指数级下降
15+
}
16+
```
17+
18+
**最佳实践 1:使用 `System.Collections.Generic.List[T]`**
19+
20+
```powershell
21+
$results = [System.Collections.Generic.List[Object]]::new()
22+
foreach ($item in $list) {
23+
$results.Add($item)
24+
}
25+
```
26+
27+
**最佳实践 2:直接将循环赋值给变量 (最推荐)**
28+
PowerShell 会自动处理管道输出并将其收集为数组(实际上是一个 ArrayList,最后转为 Array),这是最快且代码最简洁的方法。
29+
30+
```powershell
31+
$results = foreach ($item in $list) {
32+
# 处理逻辑
33+
$item # 输出对象
34+
}
35+
```
36+
37+
---
38+
39+
### 2. 循环结构的选择:`foreach` vs `ForEach-Object`
40+
41+
PowerShell 的管道 (`|`) 即使在内存中也有序列化/反序列化的开销,并且每个对象都会触发函数调用开销。
42+
43+
* **`ForEach-Object` (管道)**: 最慢,但内存占用低(流式处理)。
44+
* **`foreach ($x in $y)` (语句)**: 很快,但需要将所有数据先加载到内存中。
45+
* **`.ForEach()` (方法)**: 在 PowerShell 4.0+ 中针对集合的方法,速度极快。
46+
47+
**性能对比:**
48+
49+
```powershell
50+
# 慢 (适合节省内存)
51+
1..10000 | ForEach-Object { $_ * 2 }
52+
53+
# 快 (适合追求速度)
54+
foreach ($i in 1..10000) { $i * 2 }
55+
56+
# 极快 (语法稍微受限)
57+
(1..10000).ForEach({ $_ * 2 })
58+
```
59+
60+
**建议:** 除非内存受限(需要流式处理),否则在脚本内部处理大量数据时,优先使用 `foreach` 语句。
61+
62+
---
63+
64+
### 3. "Filter Left" 原则 (左侧过滤)
65+
66+
尽早在管道的左侧(数据源头)过滤数据,而不是把所有数据取回来后再用 `Where-Object` 过滤。
67+
68+
**错误做法:**
69+
70+
```powershell
71+
Get-ChildItem -Path C:\ -Recurse | Where-Object { $_.Extension -eq ".log" }
72+
# 这会获取成千上万个文件对象,然后再一个个检查扩展名。
73+
```
74+
75+
**最佳实践:**
76+
77+
```powershell
78+
Get-ChildItem -Path C:\ -Recurse -Filter "*.log"
79+
# 让文件系统驱动程序只返回你需要的文件。
80+
```
81+
82+
*这同样适用于 Active Directory (`-Filter`), Event Logs, SQL 查询等。*
83+
84+
---
85+
86+
### 4. 字符串操作优化
87+
88+
与数组类似,字符串在 .NET 中是不可变的。在循环中使用 `+=` 拼接字符串是性能灾难。
89+
90+
**错误做法:**
91+
92+
```powershell
93+
$log = ""
94+
foreach ($line in $data) {
95+
$log += $line + "`n"
96+
}
97+
```
98+
99+
**最佳实践:使用 StringBuilder**
100+
101+
```powershell
102+
$sb = [System.Text.StringBuilder]::new()
103+
foreach ($line in $data) {
104+
[void]$sb.AppendLine($line)
105+
}
106+
$log = $sb.ToString()
107+
```
108+
109+
---
110+
111+
### 5. 输出与控制台交互
112+
113+
写控制台 (`Write-Host`) 非常慢,因为它需要与主机 UI 交互。
114+
115+
* **避免** `Write-Host` 进行大量输出。
116+
* **避免** 多余的 `Write-Output`。在函数中直接放置对象就会被输出,不需要显式调用 `Write-Output`(这也有轻微开销)。
117+
* **压制输出**:如果你调用了一个有返回值的 .NET 方法但不需要结果,请使用 `[void]` 或 `$null =`,否则 PowerShell 会花时间处理该对象并试图将其写入管道。
118+
119+
```powershell
120+
# 慢
121+
$list.Add("item")
122+
# (List.Add 返回索引号,PS会把它打印到控制台,这很慢)
123+
124+
# 快
125+
[void]$list.Add("item")
126+
# 或者
127+
$null = $list.Add("item")
128+
```
129+
130+
---
131+
132+
### 6. 对象创建
133+
134+
创建自定义对象是脚本中常见的操作。
135+
136+
**错误做法 (PS 2.0 时代):**
137+
138+
```powershell
139+
$obj = New-Object PSObject
140+
$obj | Add-Member -MemberType NoteProperty -Name "ID" -Value 1
141+
```
142+
143+
**最佳实践 (PS 3.0+ 类型转换):**
144+
这比 `New-Object` 快大约 10 倍。
145+
146+
```powershell
147+
$obj = [PSCustomObject]@{
148+
ID = 1
149+
Name = "Test"
150+
}
151+
```
152+
153+
---
154+
155+
### 7. 处理大文件:`Get-Content` 的陷阱
156+
157+
`Get-Content` 默认是一行一行读取并作为单独的字符串对象发送到管道,对于大文件非常慢。
158+
159+
**优化 1:使用 `-ReadCount`**
160+
这会一次发送多行(数组)到管道,显著减少管道开销。
161+
162+
```powershell
163+
Get-Content huge.log -ReadCount 1000 | ForEach-Object {
164+
# $_ 现在是一个包含1000行的数组
165+
foreach ($line in $_) { ... }
166+
}
167+
```
168+
169+
**优化 2:使用 .NET 类 (极致性能)**
170+
如果只需要读取,直接使用 .NET 的 StreamReader。
171+
172+
```powershell
173+
[System.IO.File]::ReadAllLines("huge.log")
174+
# 或者流式读取
175+
$reader = [System.IO.File]::OpenText("huge.log")
176+
while ($null -ne ($line = $reader.ReadLine())) { ... }
177+
$reader.Close()
178+
```
179+
180+
---
181+
182+
### 8. 利用并行处理 (PowerShell 7+)
183+
184+
在 PowerShell 5.1 中,实现多线程(Runspaces)非常复杂。但在 PowerShell 7 (pwsh) 中,`ForEach-Object` 增加了 `-Parallel` 参数。
185+
186+
**场景:** 当你的操作受限于 I/O(如 Ping 多个服务器、查询 API、读写文件)而非 CPU 时。
187+
188+
```powershell
189+
# 串行 (耗时 10秒)
190+
1..10 | ForEach-Object { Start-Sleep -Seconds 1 }
191+
192+
# 并行 (耗时 约2秒,默认一次跑5个线程)
193+
1..10 | ForEach-Object -Parallel {
194+
Start-Sleep -Seconds 1
195+
} -ThrottleLimit 5
196+
```
197+
198+
*注意:对于简单的内存计算(如数学运算),并行化的启动开销反而可能让速度变慢。仅在有 I/O 等待时使用并行。*
199+
200+
---
201+
202+
### 9. 避免使用 `ScriptBlock` 作为过滤器
203+
204+
`Where-Object` 使用脚本块 `{ $_.Property -eq 'Value' }` 会为每个对象创建一个新的作用域。
205+
206+
**优化:使用比较语句 (PS 3.0+)**
207+
如果逻辑简单,不要用花括号。
208+
209+
```powershell
210+
# 较慢
211+
$users | Where-Object { $_.Age -gt 30 }
212+
213+
# 较快 (不再解析 ScriptBlock)
214+
$users | Where-Object Age -gt 30
215+
```
216+
217+
---
218+
219+
### 10. 总结清单
220+
221+
1. **测量它**:使用 `Measure-Command { 你的代码 }` 来验证优化效果。不要凭感觉猜。
222+
2. **Filter Left**:在数据源头过滤。
223+
3. **抛弃 `+=`**:使用 `[System.Collections.Generic.List[object]]` 或直接赋值循环结果。
224+
4. **少用 `Write-Host`**:仅用于给用户展示必要的进度。
225+
5. **必要时下潜到 .NET**:PowerShell 是 .NET 的封装,直接调用 `[System.Math]` 或 `[System.IO]` 通常比对应的 Cmdlet 快。

0 commit comments

Comments
 (0)