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