「原本幾分鐘就能跑完的彙總腳本,隨著資料增加,變成要花 3 小時」── 這是 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 中的效能基準測試(benchmark)在自己的環境上測量秒數。不過,筆數增為 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++ }
}
不過請注意預設字元編碼不同這一點。只帶 1 個參數的 ReadLines($path) 會以 UTF-8 讀取(若有 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 檔案,即使用只帶 1 個參數的多載,結果也會一致。替換之前,請務必先確認目標檔案的字元編碼與執行環境的版本。
判斷的大致標準如下。
| 情況 | 選擇 |
|---|---|
| 筆數不多・優先考慮易讀性 | 管線 |
傳入指令結果這種巨大輸入(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的格式字串產生、正規表示式的編譯、模組的重新載入等搬到迴圈外,有時就能看到效果 ── 效果程度: 中~大。取決於被搬出的那 1 次處理本身的成本,如果混入像模組重新載入這種較重的處理,效果會非常顯著 - 過度使用
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 種效能基準測試(benchmark)與測量輔助工具。
本文的範例,已在 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 的陣列是固定長度,無法追加元素。+= 運算子會展開成「建立一個新陣列、複製既有的所有元素、再把新元素加到最後」這樣的處理。每追加 1 筆就會複製全部元素,所以追加 n 筆時,運算量會與 n 的平方成正比。100 筆的話不會察覺,但超過 1 萬筆就會慢到有感,10 萬筆則已經不堪實用。解法是改用 System.Collections.Generic.List[T] 的 Add,或是改成把整個迴圈的輸出直接交給變數接收的寫法。
- 用 Measure-Command 測量的結果,和實際的執行時間對不上。
- 這是因為 Measure-Command 只測量指令碼區塊的執行時間,其輸出會被捨棄。實際執行時,還會多出把結果格式化顯示在畫面上的成本(格式化處理),以及主控台的描繪時間。此外,第一次執行會包含模組載入與 JIT 編譯的時間,因此第 1 次的測量值大多不能拿來當參考。請務必遵守兩點:把同樣的處理執行數次,只看第 2 次以後的數值;以及比較的對象要在相同條件下測量。
- 兩份 CSV 的比對一直跑不完,該修哪裡?
- 很可能是內層迴圈每次都用 Where-Object 做線性搜尋。如果雙方各有 1 萬筆,比較次數就會來到 1 億次。把其中一方轉成雜湊表(或用 Group-Object -AsHashTable)、改成用鍵值查找的形式,比較次數就能降到與總筆數成正比的程度。這是筆數愈多效果愈明顯、投資報酬率最高的改善。
- 聽說直接呼叫 .NET 的 API 比用 Cmdlet 快,是不是任何時候都該這樣做?
- 不,這要看情況分開使用。.NET 的 API(例如 System.IO.File 的 ReadLines、EnumerateFiles)確實較快,但會失去 PowerShell 提供者功能、萬用字元、相對路徑解析等便利性,程式碼也會變得比較難讀。請先測量,只替換掉實際確認為瓶頸的部分。像掃描數十萬個檔案、讀取巨大檔案這種明確有效的場合,才是實務上該限定使用的範圍。
- 把處理並行化就會變快嗎?
- 如果是等待時間佔大部分的處理,並行化確實有效,但就順序而言它排在最後。請先做能減少無謂處理的事(縮小取得筆數、把迴圈裡不變的處理搬到外面、把比對改成雜湊化)。演算法本身還是 O(n^2) 的話,就算並行化,也只會依核心數的比例變快。反過來說,如果把每筆都很輕量的處理拿去並行化,也可能因為額外負擔而變得更慢。