tech.guitarrapc.cóm

C#, PowerShell, Unity, Cloud, Serverless Technical Update and Features

PowerShellでFor / Foreach / Foreach-Object / ScriptBlock / Filterのベンチマーク

面白そうだったので、PowerShellでも試してみました。
C# - For Vs Foreach - Benchmark 2013

はじめに

PowerShellで、ForやForeachではどちらがより高速な結果を出すのか、イマイチまとまった情報を見つけることができませんでした。 そこで、PowerShellでも元サイトの考えをベースに、intでベンチマークを取っています。 本来ならPowerShell 1.0, 2.0の結果も併記するべきなのでしょうが、PowerShell 3.0のみです。 どうかご了承下さい。

測定対象

PowerShellでも、他言語同様にループ手段が複数ありますが…最低限押さえるといいかなと思ったやつを…… forforeach以外にも多用される、Foreach-Objectコマンドレット、ScriptBlockfilterを対象に測定しています。
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  
Collection Type別Chartです。 Chart1 Loop Type別Chartです。 LoopType

結果

コレクション別速度順位

  1. ArrayList (Fastest)
  2. int
  3. List (Slowest)

Data Type別 Loop操作

以下のいずれのデータタイプにも同じ傾向が見えます。
  • int (Array)
  • List
  • ArrayList
Foreachが最速で、次いでScriptBlock、僅差でFilter、大きく遅れてForeach-Objectです。
  1. Foreach (Fastest)
  2. ScriptBlock
  3. Filter
  4. 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で代用が出来ますね。 牟田口先生も、次のようにおっしゃっています。 よって、
  • ログ処理など、データが大きく単純に速度を求めてパイプに仕込まないなら……foreach
  • データが大きい中でパイプラインに仕込むなら……Filter
  • 単独で繰り返し実行…かつforeachr使わないようなときはさくっと……ScriptBlock
  • オブジェクトをパイプラインを超えて渡し、速度を求めない/速度が気にならない程度の量なら……Foreach-Object
という感じでしょうか…。

コレクションの大きさで実行速度は指数関数的に変化するのか

コレクションの大きさでは、比例して変わるのみで指数関数的な負担にはならないようです。 これなら実行速度が予想できますね。 なお、コレクションの大きさによって…… Foreachが最速で、次いでScriptBlock、僅差でFilterが状況によって入れ替わる、大きく遅れてForeach-Objectと。
  1. Foreach (Fastest)
  2. ScriptBlock
  3. Filter
  4. 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
Chart3
計測単位 : 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
Chart4
計測単位 : 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
Chart5
計測単位 : 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
Chart6
計測単位 : 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
Chart7
計測単位 : 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
Chart8

テストに利用したコード

純粋にコード部分の実行速度を測るため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)とします。

次の測定時はこれも含めてみよう

Host出力のコスト

今回は、変数に格納しているので気になりませんが、結構コストがかかります。 その対策は……ww