更新履歴(4件・最終更新 2026年08月02日)
この記事に加えた変更の記録です。アーカイブした更新前のバージョンは、DOI付きの固定URLから読めます。
- 記事の冒頭に「この記事の知識マップ」節を追加しました。本文で扱っている概念とその関係を、要約・図・詳細ページへのリンクにまとめたものです。本文の主張は変えていません。
- 外部レビュー(1283件)への対応として本文を更新しました。個々の変更内容は、この下の履歴を参照してください。
- `Invoke-KsAggregate`などが読者の処理に読み替える仮の関数名であることを明記し、対象環境(5.1と7の両対応)を追記しました。判断表に本文への参照列を足し、5つの項目に効果の目安とその根拠を追加しました。あわせて、書き方から一意に決まる処理回数の表を2つ追加しています(`+=`の要素コピー総数と、突合の比較回数)。実行時間の実測値は測っていないため載せていません。
- 本文中の関連記事へのリンクの文言が、リンク先の現在のタイトルと食い違っていたのを、実際のタイトルに揃えました。本文の内容は変えていません。
- 初版公開
この記事を引用する(DOI(登録済みアーカイブ): 10.5281/zenodo.21547445)
以下のDOIは過去に登録されたアーカイブを指しており、現在の本文とは一致しない場合があります。現在の本文を参照するときは、このページのURLを使用してください。
小村 豪(2026)「PowerShellスクリプトが遅いときに見るところ ── 配列・パイプライン・突合の勘所」合同会社小村ソフト. https://comcomponent.com/blog/powershell-performance-tuning/
- DOI(登録済みアーカイブ)
- 10.5281/zenodo.21547445
- DOI(前回登録した版)
- 10.5281/zenodo.21733154
「データが増えたら、集計スクリプトが数分では終わらなくなった」。こんなとき、最初からPowerShellを別の言語に書き直したり、処理を並列化したりする必要はありません。まず調べたいのは、件数が増えるほど無駄な処理も増える書き方になっていないかです。
代表例は、配列への繰り返し追加と、突合のたびにマスター全件を探す処理です。この記事では、計測 → 処理回数の削減 → 1件あたりのコストとI/Oの改善 → 並列化の判断の順に、遅い箇所を見つけて直す方法を整理します。
対象はWindows PowerShell 5.1とPowerShell 7の両方です。文字コードなど、バージョンによって挙動が変わる箇所は本文で区別します。配布サンプルにはPowerShell 7.6での実行検証とPester 8件のテストがありますが、この記事の処理回数の表は実行時間の実測ではありません。秒数や速度の順位は、使うバージョンとデータで測ってください。
1. まず結論:症状から調べる箇所を選ぶ
高速化で優先するのは、細かい構文の置き換えよりも不要なコピー・全件走査・ファイルの開閉を減らすことです。配列はList[T]かループ出力の集約、突合はハッシュテーブル、ディレクトリ走査は-Filter、文字列の組み立ては-joinかStringBuilderを候補にします。1234
まず2章で遅い区間を確認し、該当する行から詳しい説明へ進んでください。
| 症状 | 疑う箇所 | 対処 | 詳しくは |
|---|---|---|---|
| 件数が増えると急に遅い | $array += / $string += |
List[T]、ループ出力の変数受け、-join1 |
3章 |
| 2つのデータの突合が終わらない | 二重ループの線形探索 | ハッシュテーブル / Group-Object -AsHashTable2 |
4章 |
| 巨大ファイルの読み込みが遅い | Get-Content の行オブジェクト生成 |
-Raw / -ReadCount / [File]::ReadLines5 |
6章(文字コードの注意は5章) |
| フォルダー走査が遅い | Where-Object による後段の絞り込み |
Get-ChildItem -Filter、必要なら EnumerateFiles3 |
6章 |
| 出力ファイルの書き込みが遅い | ループ内の Add-Content |
一括書き込み、または StreamWriter4 |
6章 |
| CPUは暇なのに終わらない | ネットワーク・I/O待ち | 並列化の出番 | 別記事「並列処理」 |
| 画面表示が遅い | フォーマット処理・描画 | Format-* は最後だけ、$ProgressPreference を無効化6 |
7章 |
| どこが遅いか分からない | 計測不足 | Stopwatch で区間計測、Measure-Command は2回目以降7 |
2章 |
表の「CPUは暇なのに終わらない」も、すぐに並列化するという意味ではありません。取得件数や往復回数、ループ内の無駄を減らしたあとで、まだ待ち時間が支配的なら8章の判断へ進みます。Format-*による表示は最後に寄せ、測定から漏れる描画コストにも注意します。67
図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全21件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle
2. まず測る ── Measure-Commandの正しい使い方
推測で書き換える前に、全体でどれだけかかり、どの区間が時間を使っているかを測ります。全体の比較にはMeasure-Command、処理を区切るにはStopwatchを使うと整理できます。7
コード例の読み方。Invoke-KsAggregate、ConvertTo-KsRecord、Join-KsRecordは、読者の処理に読み替えるための仮の関数名です。標準コマンドレットではないため、そのまま貼り付けると「用語が認識されません」で止まります。自分の関数や処理に置き換えてください。そのまま動かせるベンチマークと計測ヘルパーは、末尾のサンプルzipにまとめています。
2.1. 初回を分け、同じ条件で複数回測る
# 1回目はモジュール読み込みやJITが含まれるので捨てる
$null = Measure-Command { Invoke-KsAggregate }
# 2回目以降を数回測って、ばらつきも見る
1..3 | ForEach-Object {
(Measure-Command { Invoke-KsAggregate }).TotalSeconds
}
初回にはモジュールの読み込みやJITコンパイルが含まれます。通常の処理時間を比較するときは初回の値を混ぜず、2回目以降を数回測ってばらつきも見ます。
もう1つ揃えるのが、ファイルキャッシュの状態です。I/Oを含む処理では、キャッシュが温まっているかどうかで時間が大きく変わります。比較対象は同じ条件で測ってください。
2.2. 計測値と画面での体感を分ける
Measure-Commandはスクリプトブロックの出力を破棄します。結果を画面へ整形・描画するコストは、その測定には含まれません。計測値は短いのに実際の操作が遅いなら、表示側も疑います。7
これは、測定対象の中へ明示的に入れた表示処理まで消える、という意味ではありません。通常の結果表示と、処理内の進捗更新などを分けて考え、後者は7章で確認します。
2.3. 読み込み・索引作成・突合を区間計測する
$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. 最大の犯人 ── 配列の +=
3.1. 追加のたびに、どれだけコピーしているか
PowerShellの配列は固定長です。$array += $itemは既存の配列をその場で伸ばすのではなく、新しい配列へ既存の要素をコピーして追加します。1件ずつ追加するループでは、このコピーの積み重ねが問題になります。1
空の配列へn件を1件ずつ追加するとき、既存要素のコピー総数はn(n-1)/2です。新しく追加する要素そのものではなく、既に入っている要素を移し直す回数を数えています。
| 追加する件数 | += で発生する要素コピーの総数 |
List[T].Add の追加回数 |
|---|---|---|
| 1,000件 | 約50万回 | 1,000回 |
| 10,000件 | 約5,000万回 | 10,000回 |
| 100,000件 | 約50億回 | 100,000回 |
表は実行時間の実測ではなく、この書き方から計算した処理回数です。件数を10倍にすると、繰り返しコピーはおよそ100倍に増えます。実際に何秒かかるかは、配布zipのベンチマークを自分の環境で実行して確認してください。
なお、PowerShell 7.5ではobject[]への追加が最適化されています。実装の高速化と、件数を増やしたときの処理量は別の話です。List[T]がすべての版・件数で必ず最速だと決めつけず、ループ出力の集約も含めて測る、というのが本稿の立場です。1
3.2. List[T]か、ループ出力の集約へ置き換える
# 【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 # 出力がそのまま集約される
}
List[T]は追加を繰り返す用途に向くコレクションです。容量の拡張時にはコピーが必要ですが、毎回全要素をコピーする配列追加とは違い、追加1回あたりのコストを平均して抑えられます。
【OK-2】は、ループの出力をまとめて変数へ受ける書き方です。foreach文だけでなく、ForEach-Objectやifの出力も集約できます。「出力ストリーム」の理解とセットで身につけると、結果を作るためだけの+=を避けやすくなります。
3.3. 文字列の繰り返し連結も分けて直す
文字列は不変(immutable)なので、$s += "line"も毎回新しい文字列を作ります。単純に行を連結するなら-join、条件分岐が絡む組み立てならStringBuilderを使います。4
# 【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()
置き換えるときは末尾の改行も照合してください。この例の-joinは要素の間に区切りを入れ、AppendLineは各行の末尾に改行を付けます。速さだけでなく、必要な出力形式が保たれていることを確認します。
4. 二重ループをやめる ── 突合はハッシュテーブルで
4.1. 1行処理するたびに、マスター全件を探していないか
「受注データの各行について、マスターから商品名を引く」処理を、内側のWhere-Objectで書くとこうなります。
# 【NG】受注1万件 × マスター1万件 = 1億回の比較
foreach ($order in $orders) {
$item = $master | Where-Object { $_.Code -eq $order.Code } # 毎回全件走査
$order | Add-Member NoteProperty ItemName $item.Name
}
受注1万件、マスター1万件なら比較は1億回です。外側のループを少し速くするより、内側の全件走査をなくすほうが効きます。片方の索引を一度だけ作り、その後はキーで参照します。2
4.2. 索引を一度だけ作り、キーで引く
# 【OK】索引を1度だけ作り、あとはキーで引く
$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件ずつ引きます。全体の処理量は、おおむね両者の合計件数に比例します。
| 受注件数 × マスター件数 | 二重ループの比較回数 | ハッシュテーブルの処理回数 |
|---|---|---|
| 1,000 × 1,000 | 100万回 | 約2,000回 |
| 10,000 × 10,000 | 1億回 | 約2万回 |
| 100,000 × 100,000 | 100億回 | 約20万回 |
件数を10倍にしたとき、二重ループは100倍、索引を使う形はおおむね10倍です。ここも秒数ではなく処理回数の比較です。件数が多いほど差が開くため、データが増える見込みのある突合ほど先に直す価値があります。
4.3. 重複キー・一意キー・存在確認を区別する
Group-Object -AsHashTableは同じキーの要素を配列へまとめるので、ハッシュテーブルの値は配列です。上の例は$hit[0].Nameで先頭を使い、未登録なら$nullにしています。同じキーの全件を保持することと、そのうちどれを使うかは別だと読んでください。2
キーが一意だと分かっているなら、自分でハッシュテーブルを組み立てる方法もあります。
$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文
5.1. 1件あたりのコストを減らす
パイプラインは読みやすい一方、1オブジェクトごとにコマンドレット間を流すコストがあります。数十万件規模の内側ループでは、foreach文のほうが速いことが多く、比較する価値があります。8
# パイプライン: 読みやすく、逐次処理されるので途中結果を溜め込まない
$rows | Where-Object { $_.Status -eq 'OK' } | ForEach-Object { $_.Amount } |
Measure-Object -Sum
# foreach文: 1件あたりのオーバーヘッドが小さく、大量件数では速い
$sum = 0
foreach ($r in $rows) { if ($r.Status -eq 'OK') { $sum += $r.Amount } }
ただし、「パイプラインを全部やめる」という判断ではありません。処理件数と、入力をどう保持するかを分けて考えます。
5.2. メモリ使用量は、foreachに渡す入力で変わる
foreach文は、丸かっこの中の式を評価してからループへ入ります。foreach ($line in (Get-Content $path))のようにコマンドの実行結果を渡すと、その時点で全件がメモリに載ります。8
一方、遅延列挙するIEnumerableを渡せば、1件ずつ列挙できます。巨大なファイルでも、ReadLinesを使えば全件をメモリに溜めずにforeach文で処理できます。
# 全件をメモリに載せずにforeach文で処理する(遅延列挙)
foreach ($line in [System.IO.File]::ReadLines($path)) {
if ($line.StartsWith('ERROR')) { $errors++ }
}
5.3. ReadLinesへ置き換える前に、文字コードを確認する
引数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なし)なので、UTF-8のファイルを7で扱う限り、引数1つのReadLinesでも読み取り結果が一致します。実行バージョンと、対象ファイルの文字コードを確認してから差し替えることが大切です。
使い分けをまとめると、次のようになります。
| 状況 | 選択 |
|---|---|
| 件数が少ない・読みやすさ優先 | パイプライン |
コマンドの結果を渡す巨大な入力(Get-Content など) |
パイプライン、または [IO.File]::ReadLines + foreach 文 |
| すでにメモリ上にある配列を数十万件回す | foreach 文 |
| 配列に対する単純な絞り込み | .Where({...}) / .ForEach({...}) メソッド1 |
.Where()と.ForEach()はPowerShell 4.0で追加された配列のメソッドです。パイプラインを経由しないぶん軽量ですが、対象が既にメモリ上にあるコレクションであることが前提です。1
6. ファイルとディレクトリのI/O
6.1. 読み込み:必要な単位で受け取る
Get-Contentは既定で1行ずつオブジェクトを生成するため、巨大ファイルではこのコストが支配的になることがあります。必要なのが全文か、行のまとまりか、1行ずつの処理かで選びます。5
Get-Content $path # 1行ずつ(行数分のオブジェクト生成)
Get-Content $path -Raw # 全体を1つの文字列として一括読み込み
Get-Content $path -ReadCount 1000 # 1000行ずつの配列として渡す(オブジェクト生成を削減)
[System.IO.File]::ReadLines($path) # .NET。逐次列挙で最も軽い(既定はUTF-8)
-Rawは全文を1つの文字列として読み、-ReadCountは指定した行数の配列を渡します。速度だけでなく、後続処理へ渡す単位も変わる点に注意してください。全件を保持したくないならReadLinesを検討し、5.3節の文字コード条件も併せて確認します。
6.2. ディレクトリ走査:取得してから捨てず、取得時に絞る
Get-ChildItemでは-Filterを使います。プロバイダーがオブジェクトの取得時に絞り込むため、取得した後で捨てるより効率的です。3
# 【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')
.NETのAPIへ全部置き換える必要はありません。コマンドレットを外すと、PowerShellのプロバイダー機能、ワイルドカード、相対パスの解決といった利便性を失い、読みやすさも変わります。数十万ファイルの走査など、実測でボトルネックだと分かった箇所だけを対象にします。
6.3. 書き込み:ループのたびに開閉しない
Add-Contentをループ内で都度呼ぶと、呼び出しのたびにファイルを開いて閉じます。まとめて書くか、逐次書き出しが必要ならStreamWriterを開いたまま使います。
# 【NG】1万行 = 1万回のオープン/クローズ
foreach ($r in $result) { Add-Content -Path $out -Value ($r -join ',') }
# 【OK-1】まとめて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() }
StreamWriterの例では、例外が起きても閉じられるようにfinallyでDispose()しています。書き込み方法を変更するときも、出力先・文字コード・行の形式を確認してください。
ネットワーク越しでは、ファイル操作の往復回数そのものが支配的になります。UNCパス特有の注意点は「ネットワーク共有・UNCパスの落とし穴」で扱っています。
7. 見落としがちな小さいコスト
配列・突合・I/Oを調べたら、1回あたりのコスト × 実行回数で積み上がる処理を見ます。下表の「効果の目安」は実測値ではなく、自分の処理件数へ当てはめるための目安です。
| 見る箇所 | 対処 | 効果の目安と理由 |
|---|---|---|
Write-Progressの更新頻度 |
100件ごとなどに間引く。無人実行なら$ProgressPreference = 'SilentlyContinue'で止める |
大。毎回の描画が処理本体より重いことがある。数万回以上のループでは優先して見る |
途中のFormat-Table / Format-List |
画面表示の最後だけに置く | 中。フォーマット用オブジェクトに変わり、後続処理が壊れる実害もある6 |
| ループ内の不変な処理 | 日付の書式文字列生成、正規表現のコンパイル、モジュールの再読み込みなどを外へ出せないか確認する | 中〜大。重い処理を何度も繰り返していれば大きく効く |
Select-Object -Propertyの多用 |
必要な段階まで元のオブジェクトを保つ | 小〜中。新しいPSCustomObjectを生成するため、数千件より数万件以上で効きやすい |
| 例外の多発 | 存在しないファイルを毎回取得して例外を捕捉する設計なら、Test-Pathなどで分岐する |
大。通常の分岐より送出・捕捉が重く、件数×例外率が大きいほど支配的になる |
Format-*を途中から外すのは、速度だけでなくデータを正しく後段へ渡すための修正です。また、ループの外へ出せるのはそのループ内では変わらない処理だけです。
事前の存在確認は、想定内の「無い」を通常の分岐にするためのものです。確認後の状態変化や別の失敗もあるため、必要なエラー処理まで削除する意味ではありません。
8. 並列化を考えるのは、無駄を減らしてから
まず、取得件数を絞り、ループ内の不変処理を外へ出し、配列の繰り返しコピーや突合の全件走査を直します。その後で再計測し、ネットワークやI/Oの待ち時間が支配的なら並列化を検討します。
計算量がO(n²)のままでは、並列化しても処理量の増え方は変わりません。コア数の分を狙う前に、アルゴリズムを直すほうが先です。反対に、1件が軽い処理は並列化のオーバーヘッドで遅くなることもあります。
比較するときは、時間だけでなく、突合の結果、未登録の扱い、読み書きした文字列やファイルの形式が意図どおりかも確認してください。同じ処理を速くしたのか、処理内容が変わっただけなのかを分けるためです。
具体的な選び方は「PowerShellの並列処理 ── ForEach-Object -Parallelとジョブの使い分け」で説明しています。
9. まとめ
最初にMeasure-CommandとStopwatchで測り、どの区間が遅いかを決めてから直します。初回のモジュール読み込み・JIT、ファイルキャッシュ、結果の画面表示は、比較条件として分けて扱います。7
改善の中心は、繰り返す仕事そのものを減らすことです。配列追加はList[T]かループ出力の集約、文字列連結は-joinかStringBuilderへ。突合は、毎回の全件走査から一度だけの索引作成へ変えます。処理回数の表は、秒数の保証ではなく、件数が増えたときの伸び方を見るために使ってください。124
ファイルI/Oは読み込み単位・取得時の絞り込み・書き込み時の開閉回数を見直します。foreach文へ替える場合は入力の保持方法と文字コードも確認します。さらに表示・進捗・オブジェクト生成などを調べ、並列化は最後に判断します。8536
サンプルコードのダウンロード
この記事で扱ったコードは、そのまま動かせる形にまとめて配布しています。3種類のベンチマークと、計測ヘルパーが入っています。
この記事のサンプルは、PowerShell 7.6 で実際に実行して検証しています(Pester 8件)。zipに含まれる Invoke-SampleTests.ps1 を実行すれば、お手元でも同じ検証を再現できます。
# 構文解析 + 静的解析 + Pesterテスト
./Invoke-SampleTests.ps1
設定値(パス、サーバー名、テナントIDなど)は例です。そのまま本番環境で実行せず、自社の環境に合わせて読み替えてください。
関連記事
- PowerShellの並列処理 ── ForEach-Object -Parallelとジョブの使い分け
- Write-Hostをやめる ── PowerShellの出力ストリームとログ設計
- PowerShellでExcel・CSV業務処理を自動化する ── 集計・突合・帳票出力の実務レシピ
- PowerShell実用コマンド集 ── 日常作業でよく使う小さな機能を増やす
- Windowsでプログラムのバージョン別速度を正しく比較する方法
- PerfViewとdotnet-traceで「遅い」を特定する
関連する相談領域
合同会社小村ソフトでは、時間がかかりすぎる集計・突合処理の高速化、データ量の増加に耐える処理設計への作り替え、性能劣化の原因調査を扱っています。
参考リンク
-
Microsoft Learn, about_Arrays. PowerShellの配列が固定サイズであり、+= 演算子が新しい配列を作成して既存の要素をコピーしたうえで要素を追加する動作になること、要素の追加・削除を繰り返す場合はコレクション型(List など)の使用が適していること、PowerShell 4.0で追加された .Where() および .ForEach() メソッドについて。 バージョンによる速度差についてはPowerShell 7.5の新機能も参照。object[]への1件追加の実装はPowerShell 7.6.0のArrayOps.AddObjectArrayで確認でき、Array.Resizeで長さを1増やしている。最適化の有無と、本稿の処理回数のモデル・実測時間は区別する。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
Microsoft Learn, Group-Object. -AsHashTableによりグループ化結果をハッシュテーブルとして返せること、-AsStringによりキーを文字列として扱えること、返されるハッシュテーブルの値が各グループの要素の配列になることについて。 ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Get-ChildItem. -Filterがプロバイダーによってオブジェクト取得時に適用されるため、取得後にフィルタリングする他のパラメーターより効率的であること、-Fileや-Recurseとの組み合わせについて。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, StringBuilder クラス. Stringが不変であるため連結のたびに新しいインスタンスが生成されるのに対し、StringBuilderが可変のバッファ上で文字列を組み立てることについて。あわせてStreamWriter クラスのストリームを開いたまま逐次書き込む動作について。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Get-Content. 既定では改行区切りで1行ずつオブジェクトとして返すこと、-Rawによりファイル全体を1つの文字列として読み込めること、-ReadCountにより指定行数ずつまとめてパイプラインへ送れることについて。 ↩ ↩2 ↩3
-
Microsoft Learn, Format-Table. Format系コマンドレットが表示用のフォーマットオブジェクトを生成するため、後続のコマンドへパイプする用途には適さず、パイプラインの最後で使うべきであることについて。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Measure-Command. スクリプトブロックやコマンドの実行時間を測定しTimeSpanを返すこと、測定対象の出力自体は結果に含まれないことについて。あわせてStopwatch クラスによる区間計測について。 ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, about_Foreach. foreach文が丸かっこ内の式を評価してから反復を開始するため、コマンドの実行結果を渡すとその結果がメモリに保持される一方、ForEach-Objectコマンドレットはパイプラインから逐次入力を受け取ること、両者の使い分けについて。遅延列挙の例としてはFile.ReadLines メソッドが、ファイル全体を読み込まずに1行ずつ列挙できること(ReadAllLinesとの違い)を説明している。 ↩ ↩2 ↩3
関連する記事
同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。
PowerShellの並列処理 ── ForEach-Object -Parallelとジョブの使い分け
ForEach-Object -Parallel・Start-ThreadJob・Start-Jobの違いと使い分け、$using:とスレッド安全性、ThrottleLimitの決め方、かえって遅くなるケースまでを実務目線で整理します。
Write-Hostをやめる ── PowerShellの出力ストリームとログ設計
PowerShellの6つの出力ストリームの使い分け、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技術トピック
Windows 開発、不具合調査、既存資産活用の技術トピックをまとめた入口です。
このテーマがつながるサービス
この記事は次のサービスページにつながります。近い入口からご覧ください。
Windowsアプリ開発
業務アプリ、装置連携、通信ツールなどの Windows ソフト開発を支援します。
よくある質問
この記事のテーマについて、相談時によくある質問をまとめています。
- $array += $item という書き方は、なぜ遅いのですか?
- PowerShellの配列は固定長で、要素を追加できないためです。+= 演算子は「新しい配列を作り、既存の全要素をコピーし、末尾に追加する」という処理に展開されます。1件追加するたびに全件コピーが走るので、n件追加すると計算量はnの2乗に比例します。100件なら気づきませんが、1万件を超えると体感できるほど遅くなり、10万件では実用に耐えません。対処は、System.Collections.Generic.List[T]を使ってAddするか、ループ全体の出力を変数に受ける書き方に変えることです。
- Measure-Commandで測ったのに、実際の実行時間と合いません。
- Measure-Commandはスクリプトブロックの実行時間だけを測り、その出力は破棄されるためです。実際の実行では、結果を画面に整形して表示するコスト(フォーマット処理)や、コンソールへの描画時間が加わります。また、初回実行にはモジュールの読み込みやJITコンパイルの時間が含まれるため、1回目の測定値は多くの場合あてになりません。同じ処理を数回実行して2回目以降の値を見る、比較対象は同じ条件で測る、という2点を守ってください。
- 2つのCSVの突合が終わりません。どこを直せばよいですか?
- 内側のループで毎回Where-Objectによる線形探索をしている可能性が高いです。件数がそれぞれ1万件なら、比較回数は1億回になります。片方をハッシュテーブル(またはGroup-Object -AsHashTable)に変換してキーで引く形にすると、比較回数は合計件数に比例する程度まで落ちます。件数が増えるほど効果が大きくなる、最も費用対効果の高い改善です。
- コマンドレットより.NETのAPIを直接呼んだほうが速いと聞きました。常にそうすべきですか?
- いいえ、使い分けです。.NETのAPI(例: System.IO.FileのReadLinesやEnumerateFiles)は確かに高速ですが、PowerShellのプロバイダー機能やワイルドカード、相対パスの解決といった利便性を失い、コードも読みにくくなります。まずは測って、実際にボトルネックだと分かった箇所だけを置き換えてください。数十万ファイルの走査や巨大ファイルの読み込みのように、明確に効く場面に限定するのが実務的です。
- 処理を並列化すれば速くなりますか?
- 待ち時間が支配的な処理なら効果がありますが、順番の問題として最後です。まずは無駄な処理を減らすこと(取得件数を絞る、ループの中から不変な処理を追い出す、突合をハッシュ化する)を先にやってください。アルゴリズムがO(n^2)のまま並列化しても、コア数の分しか速くなりません。逆に、1件が軽い処理を並列化すると、オーバーヘッドで遅くなることもあります。