面白そうだったので、PowerShellでも試してみました。
C# - For Vs Foreach - Benchmark 2013
はじめに
PowerShellで、ForやForeachではどちらがより高速な結果を出すのか、イマイチまとまった情報を見つけることができませんでした。 そこで、PowerShellでも元サイトの考えをベースに、intでベンチマークを取っています。 本来ならPowerShell 1.0, 2.0の結果も併記するべきなのでしょうが、PowerShell 3.0のみです。 どうかご了承下さい。
測定対象
PowerShellでも、他言語同様にループ手段が複数ありますが…最低限押さえるといいかなと思ったやつを…… for
とforeach
以外にも多用される、Foreach-Object
コマンドレット、ScriptBlock
、filter
を対象に測定しています。
for foreach Foreach-Object ScriptBlock Filter
いわずとしてれますが、PowerShellで利用する際はこのような感じです。
for (<初期化パイプライン>; <評価パイプライン>;<インクリメントパイプライン>){<文リスト>} foreach (<変数> in <ループにかけるパイプライン>){<文リスト>} <パラメータ> | Foreach-Object {begin}{process}{end} &{文リスト} filter <名前>{param(<パラメーターリスト>) <文リスト>}
ベンチマーク結果
元サイト同様の基準です。
計測単位 : miliseconds コレクション長 : 30000000
Data Type | Collection Type | Loop Type | Test 1 | Test 2 | Test 3 | Average |
int | int | For | 275899.00 | 265212.00 | 283013.40 | 274708.13 |
int | Foreach | 186429.45 | 176947.98 | 198700.93 | 187359.46 | |
int | Foreach-Object | 1050280.06 | 1055265.18 | 1035663.52 | 1047069.58 | |
int | ScriptBlock | 227393.92 | 240253.13 | 224370.40 | 230672.48 | |
int | Filter | 237666.43 | 233423.09 | 233057.27 | 234715.60 | |
int | List<int> | For | 340137.03 | 355315.28 | 335932.48 | 343794.93 |
int | Foreach | 241257.27 | 260461.74 | 240227.87 | 247315.63 | |
int | Foreach-Object | 1160124.71 | 1290480.25 | 1156974.90 | 1202526.62 | |
int | ScriptBlock | 300991.85 | 322849.46 | 295052.61 | 306297.97 | |
int | Filter | 342645.71 | 308032.91 | 348444.29 | 347944.69 | |
int | ArrayList<int> | For | 258247.16 | 280059.96 | 272251.21 | 270186.11 |
int | Foreach | 162259.60 | 174109.73 | 172552.03 | 169640.45 | |
int | Foreach-Object | 997676.13 | 1079849.90 | 1057534.18 | 1045020.07 | |
int | ScriptBlock | 209276.20 | 232152.56 | 214621.41 | 218683.39 | |
int | Filter | 229675.65 | 230850.35 | 231555.98 | 231299.99 |
結果
コレクション別速度順位
- ArrayList (Fastest)
- int
- List (Slowest)
Data Type別 Loop操作
以下のいずれのデータタイプにも同じ傾向が見えます。
- int (Array)
- List
- ArrayList
Foreachが最速で、次いでScriptBlock、僅差でFilter、大きく遅れてForeach-Objectです。
- Foreach (Fastest)
- ScriptBlock
- Filter
- Foreach-Object (Slowest)
実際にループで使うならどれにするか
正直意外だったのは、Foreach-Objectのコストの高さです。
Data Type | Collection Type | Loop Type | Average | v.s. int For | v.s. int |
int | int | For | 274708.13 | - | - |
int | Foreach | 187359.46 | 68.20% | - | |
int | Foreach-Object | 1047069.58 | 381.16% | - | |
int | ScriptBlock | 230672.48 | 83.97% | - | |
int | Filter | 234715.60 | 85.44% | - | |
int | List<int> | For | 343794.93 | 125.15% | 125.15% |
int | Foreach | 247315.63 | 90.03% | 132.00% | |
int | Foreach-Object | 1202526.62 | 437.75% | 114.85% | |
int | ScriptBlock | 306297.97 | 111.50% | 132.78% | |
int | Filter | 347944.69 | 126.66% | 148.24% | |
int | ArrayList<int> | For | 270186.11 | 98.35% | 98.35% |
int | Foreach | 169640.45 | 61.75% | 90.54% | |
int | Foreach-Object | 1045020.07 | 380.41% | 99.80% | |
int | ScriptBlock | 218683.39 | 79.61% | 94.80% | |
int | Filter | 231299.99 | 84.20% | 98.54% |
まさか、forの4倍、foeachの5-6倍、ScriptBlock/Filterの4-4.5倍とは…… また、ScriptBlockがforeachに次いで早く、Filterもほぼ同速度。 Foreach-Objectは実態としてFilterだったとインアクションで読んだような気がしましたが…大きく異なる結果でした。 これだと、むしろScriptBlockの{process{}}を仕込んでいるだけ + PSObject:Functionに登録する/呼び出す手間程度でしょうか? foreachを書くのはパイプに仕込めずイラッとしますが、速度を求める場合は、Filterで代用が出来ますね。 牟田口先生も、次のようにおっしゃっています。
@guitarrapc ForEach-Objectというかパイプ内のprocess{}って、コマンドレットクラスのProcessRecord()メソッドを実行していて、出力がある場合はそのメソッド内でWriteObject()メソッドを呼んでるので相当コストは高いと思います。
— 牟田口大介 (@mutaguchi) March 5, 2013
@guitarrapc あとForEach-Objectの場合はProcessRecord()内でscriptblockのInvokeを毎回やってるので、そのコストも高そうです。(何らかの最適化はしてるかもしれませんが)
— 牟田口大介 (@mutaguchi) March 5, 2013
速度重視の場合はForEach-Object使わない方がいいかもなあ。あとパイプライン入力が重要な場合以外は。1..1000|%{}とかは書きやすいから良くやるけど速度的にはね…
— 牟田口大介 (@mutaguchi) March 5, 2013
もっというと、配列とかコレクション|ForEach-Objectはあんまりやらないほうがよくて、コマンドレットや関数|ForEach-Objectの場合のみにする、という感じかな。process{}を実行することが重要な場合のみ利用、と。
— 牟田口大介 (@mutaguchi) March 5, 2013
@guitarrapc スクリプトブロックがこんなに速いってことは、ForEach-Objectが遅い理由って.NETの世界をぐるぐる回るから、ってことなんですかねー。高度な関数版も比較してみると何かわかるのかも。
— 牟田口大介 (@mutaguchi) March 5, 2013
よって、
- ログ処理など、データが大きく単純に速度を求めてパイプに仕込まないなら……foreach
- データが大きい中でパイプラインに仕込むなら……Filter
- 単独で繰り返し実行…かつforeachr使わないようなときはさくっと……ScriptBlock
- オブジェクトをパイプラインを超えて渡し、速度を求めない/速度が気にならない程度の量なら……Foreach-Object
という感じでしょうか…。
コレクションの大きさで実行速度は指数関数的に変化するのか
コレクションの大きさでは、比例して変わるのみで指数関数的な負担にはならないようです。 これなら実行速度が予想できますね。 なお、コレクションの大きさによって…… Foreachが最速で、次いでScriptBlock、僅差でFilterが状況によって入れ替わる、大きく遅れてForeach-Objectと。
- Foreach (Fastest)
- ScriptBlock
- Filter
- Foreach-Object (Slowest)
コレクションの長さが、10,100,1000,10000,100000,1000000による結果を示します。
計測単位 : miliseconds コレクション長 : 10
Data Type | Collection Type | Loop Type | Test 1 | Test 2 | Test 3 | Average |
int | int | For | 0.15 | 0.22 | 0.21 | 0.19 |
int | int | Foreach | 0.24 | 0.15 | 0.11 | 0.17 |
int | int | Foreach-Object | 0.57 | 0.62 | 0.67 | 0.62 |
int | int | ScriptBlock | 0.19 | 0.26 | 0.22 | 0.22 |
int | int | Filter | 0.21 | 0.20 | 0.19 | 0.20 |
int | List<int> | For | 0.19 | 0.24 | 0.16 | 0.20 |
int | List<int> | Foreach | 0.13 | 0.33 | 0.13 | 0.19 |
int | List<int> | Foreach-Object | 0.48 | 0.66 | 0.64 | 0.59 |
int | List<int> | ScriptBlock | 0.30 | 0.28 | 0.24 | 0.27 |
int | List<int> | Filter | 0.34 | 0.18 | 0.19 | 0.24 |
int | ArrayList<int> | For | 0.12 | 0.23 | 0.13 | 0.16 |
int | ArrayList<int> | Foreach | 0.09 | 0.19 | 0.10 | 0.13 |
int | ArrayList<int> | Foreach-Object | 0.42 | 0.77 | 0.50 | 0.56 |
int | ArrayList<int> | ScriptBlock | 0.15 | 0.25 | 0.20 | 0.20 |
int | ArrayList<int> | Filter | 0.16 | 0.19 | 0.15 | 0.17 |
計測単位 : miliseconds コレクション長 : 100
Data Type | Collection Type | Loop Type | Test 1 | Test 2 | Test 3 | Average |
int | int | For | 1.08 | 0.90 | 1.08 | 1.02 |
int | int | Foreach | 0.66 | 0.67 | 0.69 | 0.67 |
int | int | Foreach-Object | 3.64 | 3.49 | 3.59 | 3.57 |
int | int | ScriptBlock | 0.94 | 0.85 | 0.96 | 0.91 |
int | int | Filter | 1.02 | 1.07 | 1.05 | 1.05 |
int | List<int> | For | 1.24 | 1.15 | 1.17 | 1.19 |
int | List<int> | Foreach | 0.92 | 2.91 | 0.97 | 1.60 |
int | List<int> | Foreach-Object | 7.69 | 4.48 | 4.21 | 5.46 |
int | List<int> | ScriptBlock | 1.16 | 1.20 | 1.07 | 1.14 |
int | List<int> | Filter | 1.26 | 1.25 | 1.05 | 1.19 |
int | ArrayList<int> | For | 1.01 | 0.99 | 1.13 | 1.04 |
int | ArrayList<int> | Foreach | 0.67 | 0.77 | 0.57 | 0.67 |
int | ArrayList<int> | Foreach-Object | 8.37 | 3.50 | 3.52 | 5.13 |
int | ArrayList<int> | ScriptBlock | 0.76 | 0.79 | 0.75 | 0.77 |
int | ArrayList<int> | Filter | 0.75 | 0.97 | 0.78 | 0.84 |
計測単位 : miliseconds コレクション長 : 1000
Data Type | Collection Type | Loop Type | Test 1 | Test 2 | Test 3 | Average |
int | int | For | 13.13 | 12.11 | 10.33 | 11.86 |
int | int | Foreach | 7.38 | 6.54 | 6.70 | 6.88 |
int | int | Foreach-Object | 41.38 | 39.61 | 37.85 | 39.61 |
int | int | ScriptBlock | 10.03 | 8.14 | 10.27 | 9.48 |
int | int | Filter | 8.97 | 9.04 | 7.33 | 8.45 |
int | List<int> | For | 15.10 | 12.45 | 13.04 | 13.53 |
int | List<int> | Foreach | 9.94 | 9.86 | 8.30 | 9.37 |
int | List<int> | Foreach-Object | 45.20 | 41.35 | 44.35 | 43.63 |
int | List<int> | ScriptBlock | 12.36 | 10.13 | 10.42 | 10.97 |
int | List<int> | Filter | 12.11 | 10.80 | 9.74 | 10.88 |
int | ArrayList<int> | For | 9.13 | 9.46 | 11.98 | 10.19 |
int | ArrayList<int> | Foreach | 6.23 | 6.95 | 6.35 | 6.51 |
int | ArrayList<int> | Foreach-Object | 39.64 | 40.04 | 37.64 | 39.11 |
int | ArrayList<int> | ScriptBlock | 7.65 | 7.15 | 7.17 | 7.32 |
int | ArrayList<int> | Filter | 6.92 | 7.19 | 9.17 | 7.76 |
計測単位 : miliseconds コレクション長 : 10000
Data Type | Collection Type | Loop Type | Test 1 | Test 2 | Test 3 | Average |
int | int | For | 105.29 | 100.37 | 97.21 | 100.95 |
int | int | Foreach | 68.30 | 66.19 | 65.25 | 66.58 |
int | int | Foreach-Object | 386.23 | 367.49 | 377.58 | 377.10 |
int | int | ScriptBlock | 87.87 | 80.90 | 83.52 | 84.10 |
int | int | Filter | 84.34 | 82.42 | 80.99 | 82.58 |
int | List<int> | For | 132.17 | 125.16 | 121.24 | 126.19 |
int | List<int> | Foreach | 90.14 | 88.64 | 87.66 | 88.81 |
int | List<int> | Foreach-Object | 425.56 | 427.77 | 417.71 | 423.68 |
int | List<int> | ScriptBlock | 112.23 | 107.41 | 108.86 | 109.50 |
int | List<int> | Filter | 102.32 | 143.87 | 104.03 | 116.74 |
int | ArrayList<int> | For | 102.66 | 94.42 | 97.98 | 98.35 |
int | ArrayList<int> | Foreach | 63.03 | 62.88 | 63.24 | 63.05 |
int | ArrayList<int> | Foreach-Object | 374.20 | 369.76 | 376.02 | 373.33 |
int | ArrayList<int> | ScriptBlock | 86.63 | 80.63 | 80.79 | 82.68 |
int | ArrayList<int> | Filter | 75.81 | 76.07 | 76.78 | 76.22 |
計測単位 : miliseconds コレクション長 : 100000
Data Type | Collection Type | Loop Type | Test 1 | Test 2 | Test 3 | Average |
int | int | For | 928.67 | 989.31 | 934.40 | 950.79 |
int | int | Foreach | 639.67 | 655.97 | 648.20 | 647.95 |
int | int | Foreach-Object | 3739.28 | 3814.27 | 3730.59 | 3761.38 |
int | int | ScriptBlock | 817.40 | 820.79 | 808.26 | 815.49 |
int | int | Filter | 818.23 | 825.11 | 838.87 | 827.41 |
int | List<int> | For | 1210.98 | 1221.01 | 1198.10 | 1210.03 |
int | List<int> | Foreach | 843.85 | 843.32 | 879.60 | 855.59 |
int | List<int> | Foreach-Object | 4280.31 | 4326.06 | 4254.13 | 4286.83 |
int | List<int> | ScriptBlock | 1075.82 | 1105.29 | 1072.61 | 1084.57 |
int | List<int> | Filter | 1060.23 | 1081.08 | 1071.59 | 1070.97 |
int | ArrayList<int> | For | 941.65 | 937.21 | 946.71 | 941.86 |
int | ArrayList<int> | Foreach | 622.43 | 593.67 | 589.76 | 601.95 |
int | ArrayList<int> | Foreach-Object | 3574.23 | 3654.80 | 3646.56 | 3625.19 |
int | ArrayList<int> | ScriptBlock | 765.73 | 818.17 | 776.87 | 786.92 |
int | ArrayList<int> | Filter | 759.23 | 747.40 | 743.89 | 750.17 |
計測単位 : miliseconds コレクション長 : 1000000
Data Type | Collection Type | Loop Type | Test 1 | Test 2 | Test 3 | Average |
int | int | For | 9744.00 | 9438.96 | 9758.17 | 9647.04 |
int | int | Foreach | 6766.58 | 6401.43 | 6640.43 | 6602.81 |
int | int | Foreach-Object | 38629.84 | 37763.99 | 38132.47 | 38175.43 |
int | int | ScriptBlock | 8480.02 | 8277.16 | 8329.73 | 8362.30 |
int | int | Filter | 8326.80 | 8178.34 | 8076.98 | 8194.04 |
int | List<int> | For | 12840.89 | 12497.00 | 12669.28 | 12669.06 |
int | List<int> | Foreach | 8913.08 | 8733.64 | 8840.35 | 8829.02 |
int | List<int> | Foreach-Object | 43677.32 | 42700.12 | 43606.07 | 43327.84 |
int | List<int> | ScriptBlock | 11180.41 | 10921.40 | 11042.95 | 11048.25 |
int | List<int> | Filter | 10552.67 | 10176.99 | 10198.66 | 10309.44 |
int | ArrayList<int> | For | 9361.94 | 9696.00 | 9582.81 | 9546.92 |
int | ArrayList<int> | Foreach | 6066.82 | 6069.82 | 6185.74 | 6107.46 |
int | ArrayList<int> | Foreach-Object | 36339.03 | 36905.48 | 37547.94 | 36930.82 |
int | ArrayList<int> | ScriptBlock | 7704.85 | 7682.98 | 7671.94 | 7686.59 |
int | ArrayList<int> | Filter | 7319.12 | 7521.99 | 7445.32 | 7428.81 |
テストに利用したコード
純粋にコード部分の実行速度を測るためMesure-Objectを仕込んでしまっています。 キャストを含めるなら、Function全体を囲むべきなのですが。。。 Filter実行用に予め設定します。
filter Get-FilterTest{ [decimal]$total += $_ }
int[]に対するForループサンプルです。
function Get-intForTest{ param( $int ) Measure-Command{ for ($i = 0; $i -lt $int.length; $i++) { [decimal]$total += $int[$i] } } }
int[]に対するForeachループサンプルです。
function Get-intForeachTest{ param( $int ) Measure-Command{ foreach ($i in $int) { [decimal]$total += $i } } }
int[]に対するForeach-Objectループサンプルです。
function Get-intForeachObjectTest{ param( $int ) Measure-Command{ $int | ForEach-Object{ [decimal]$total += $_ } } }
int[]に対するScriptBlockループサンプルです。
function Get-intScriptBlockTest{ param( $int ) Measure-Command{ $int | &{process{[decimal]$total += $_}} } }
int[]に対するFilterループサンプルです。
function Get-intFilterTest{ param( $int ) Measure-Command{ $int | Get-FilterTest } }
実行前に、コレクションを宣言します。
[int[]]$array=@() $array += 1..30000000
実行します。
(Get-intForTest -int $array).TotalMilliseconds (Get-intForeachTest -int $array).TotalMilliseconds (Get-intForeachObjectTest -int $array).TotalMilliseconds (Get-intScriptBlockTest -int $array).TotalMilliseconds (Get-intFilterTest -int $array).TotalMilliseconds (Get-intFilterTest -int $array).TotalMilliseconds
コード全文です。テストに4回連続で実行しています。
filter Get-FilterTest{ [decimal]$total += $_ } function Get-intForTest{ param( $int ) Measure-Command{ for ($i = 0; $i -lt $int.length; $i++) { [decimal]$total += $int[$i] } } } function Get-intForeachTest{ param( $int ) Measure-Command{ foreach ($i in $int) { [decimal]$total += $i } } } function Get-intForeachObjectTest{ param( $int ) Measure-Command{ $int | ForEach-Object{ [decimal]$total += $_ } } } function Get-intScriptBlockTest{ param( $int ) Measure-Command{ $int | &{process{[decimal]$total += $_}} } } function Get-intFilterTest{ param( $int ) Measure-Command{ $int | Get-FilterTest } } function Get-ListForTest{ param( $int ) $list = New-Object 'System.Collections.Generic.List`1[System.String]' $int | foreach-Object { $list.Add($_)} #,$list Measure-Command{ for ($i = 0; $i -lt $int.length; $i++) { [decimal]$total += $list[$i] } } } function Get-ListForeachTest{ param( $int ) $list = New-Object 'System.Collections.Generic.List`1[System.String]' $int | foreach-Object { $list.Add($_)} #,$list Measure-Command{ foreach ($l in $list) { [decimal]$total += $l } } } function Get-ListForeachObjectTest{ param( $int ) $list = New-Object 'System.Collections.Generic.List`1[System.String]' $int | foreach-Object { $list.Add($_)} #,$list Measure-Command{ $list | foreach-Object{ [decimal]$total += $_ } } } function Get-ListScriptBlockTest{ param( $int ) $list = New-Object 'System.Collections.Generic.List`1[System.String]' $int | foreach-Object { $list.Add($_)} #,$list Measure-Command{ $list | &{process{[decimal]$total += $_}} } } function Get-ListFilterTest{ param( $int ) $list = New-Object 'System.Collections.Generic.List`1[System.String]' $int | foreach-Object { $list.Add($_)} #,$list Measure-Command{ $list | Get-FilterTest } } function Get-ArrayListForTest{ param( $int ) $arrayList = New-Object System.Collections.ArrayList [Void]($int | foreach-Object { $arrayList.Add($_) }) #,$Arraylist Measure-Command{ for ($i = 0; $i -lt $int.length; $i++) { [decimal]$total += $arrayList[$i] } } } function Get-ArrayListForeachTest{ param( $int ) $arrayList = New-Object System.Collections.ArrayList [Void]($int | foreach-Object { $arrayList.Add($_) }) #,$Arraylist Measure-Command{ foreach ($al in $arrayList) { [decimal]$total += $al } } } function Get-ArrayListForeachObjectTest{ param( $int ) $arrayList = New-Object System.Collections.ArrayList [Void]($int | foreach-Object { $arrayList.Add($_) }) #,$Arraylist Measure-Command{ $arrayList | Foreach-Object{ [decimal]$total += $_ } } } function Get-ArrayListScriptBlockTest{ param( $int ) $arrayList = New-Object System.Collections.ArrayList [Void]($int | foreach-Object { $arrayList.Add($_) }) #,$Arraylist Measure-Command{ $arrayList | &{process{[decimal]$total += $_}} } } filter Get-ArrayListFilterTest{ param( $int ) $arrayList = New-Object System.Collections.ArrayList [Void]($int | foreach-Object { $arrayList.Add($_) }) #,$Arraylist Measure-Command { $arrayList | Get-FilterTest } } [int[]]$array=@() $array += 1..30000000 1..4 | ForEach-Object { (Get-intForTest -int $array).TotalMilliseconds (Get-intForeachTest -int $array).TotalMilliseconds (Get-intForeachObjectTest -int $array).TotalMilliseconds (Get-intScriptBlockTest -int $array).TotalMilliseconds (Get-intFilterTest -int $array).TotalMilliseconds (Get-ListForTest -int $array).TotalMilliseconds (Get-ListForeachTest -int $array).TotalMilliseconds (Get-ListForeachObjectTest -int $array).TotalMilliseconds (Get-ListScriptBlockTest -int $array).TotalMilliseconds (Get-ListFilterTest -int $array).TotalMilliseconds (Get-ArrayListForTest -int $array).TotalMilliseconds (Get-ArrayListForeachTest -int $array).TotalMilliseconds (Get-ArrayListForeachObjectTest -int $array).TotalMilliseconds (Get-ArrayListScriptBlockTest -int $array).TotalMilliseconds (Get-ArrayListFilterTest -int $array).TotalMilliseconds "" }
ライセンス
本記事のソースコードやファイルは、元サイト同様にThe Code Project Open License (CPOL)とします。
次の測定時はこれも含めてみよう
@guitarrapc 普通の関数(process{$_})、高度な関数(valuefrompipelineなパラメータ)、filter、自作ForEach-Objectコマンドレットあたりまで比較すればコンプリートかなw
— 牟田口大介 (@mutaguchi) March 5, 2013
Host出力のコスト
今回は、変数に格納しているので気になりませんが、結構コストがかかります。 その対策は……ww
@guitarrapc 出力値のホストへの整形表示もコスト高いですね。文字列出力を"文字列"とするのではなく、[console]::WriteLine("文字列")にすると劇的に速くなります。
— 牟田口大介 (@mutaguchi) March 5, 2013