各ループの動作速度がどの程度なのかをあまり把握せずになんとなく選択している状態だったので一度測ってみようと思い計測してみました。
計測
1から50万までの数字が入った配列を入力して、各要素に2をかけた数を出力するシンプルなコードで行いました。
$array = 1..500000
ForEach-Objectの場合
$array | ForEach-Object { 2 * $_ }
結果
感想
C#のコードをAdd-Type
したものが最速でした。
switch
文は入力に配列を渡すと各要素ごとに処理をするのですが、Default
ブロックのみだとC#にせまる爆速っぷり。かといってループの代わりに使うわけにもいかないわけで、なんとも歯がゆい結果に。
ForEach-Object
はダントツで遅かったです。シンプルで直感的に書けるようにするためか裏で結構複雑なことをやっているコマンドレットらしいので仕方ないのでしょうがそれにしても遅い。
呼び出し演算子(&
)がとても速いことにはちょっと驚きました。foreach
よりも若干速いくらいなので今後は積極的に使ってもいいかなと思いました。
関数のprocess
ブロックでパイプラインから入力する方法も&
と同等の速さです。filter
も同様ですね。
ドットソース演算子(.
)は呼び出し演算子(&
)よりはほんの少し遅いものの気にするほどの差はないのでこの辺りはスコープの違いのみによって使い分けてよさそうです。
foreach
文は期待通り速さだった一方でfor
文やwhile
文は意外に速くないこともわかりました。ループ毎に終了条件チェックや配列の添え字アクセスなどが生じてしまうのが原因でしょうか。foreach
文とここまで差がつくのは予想外でした。
高度な関数は普通の関数やfileter
のパイプライン入力より大幅に遅くなってしまうようです。
C#でコマンドレット(パイプライン入力)も作ってみましたが、高度な関数より3割程速くなってます。
ForEach
メソッドはForEach-Object
の2倍程度の速さです。
LINQのSelect
メソッドは気になったから入れただけですが、Powershellから使うと遅いですね。わざわざPowershellから使うことはないでしょう。
結論
今回の結果を見て今後スクリプト書く時の指針というか自分なりの結論です。
-
&
、.
、foreach
、filter
の速度は優秀なので積極的に使う。 -
さらに速度が必要な場合はC#のコードを
Add-Type
して使う。 -
for
文やwhile
文はそんなに速くないので、ループカウンタを+1して回すような単純な処理の場合はforeach
文等で代用した方がよい。 -
ForEach-Object
はシンプルで簡便に書けるのがメリットだが、とても遅いので大量のデータを入力するような場合には使うべきではない。 -
高度な関数のパイプライン入力は速くないので、単純なフィルタリングなら高度な関数にするべきではない。
今回計測したループ処理
便宜上ループって書いてますがforeach的な処理です。
ForEach-Object
$array | ForEach-Object { 2 * $_ }
foreach文
foreach ($i in $array) { 2 * $i }
呼び出し演算子 (&)
$array | & { process { 2 * $_ } }
ドットソース演算子 (.)
$array | . { process { 2 * $_ } }
関数
function TwiceFunction { process { 2 * $_ } }
$array | TwiceFunction
filter
filter TwiceFilter { 2 * $_ }
$array | TwiceFilter
高度な関数
TwiceAdvance { param([Parameter(ValueFromPipeline)][int]$v) process { 2 * $v } }
$array | TwiceAdvance
ForEachメソッド
$array.ForEach({ 2 * $_ })
for文
for ($i = 0; $i -lt $array.Length; $i++) { 2 * $array[$i] }
while文
$i = 0; while ($i -lt $array.Length) { 2 * $array[$i]; $i++ }
switch文
switch ($array) { default { 2 * $_ } }
Select (LINQ)
[System.Linq.Enumerable]::Select($array, [Func[object,int]]{ param($i) 2 * $i })
C# (Add-Type)
$src = @"
using System;
using System.Linq;
public class LoopTest
{
public static int[] Twice(object[] array)
{
return array.Select(x => 2 * (int)x).ToArray();
}
}
"@
Add-Type -TypeDefinition $src -Language CSharp
[LoopTest]::Twice($array) #
コマンドレット (C# Binary Module)
using System.Management.Automation;
namespace LoopTest
{
[Cmdlet(VerbsCommon.Get, "Twice")]
public class GetTwiceCommand : Cmdlet
{
[Parameter(ValueFromPipeline = true)]
public int InputValue { get; set; }
protected override void ProcessRecord()
{
WriteObject(2 * InputValue);
}
}
}
Import-Module "./LoopTest.dll" -Cmdlet "Get-Twice"
$array | Get-Twice
今回使ったコード全て
コピペでそのまま実行できます。(バイナリモジュールの部分はコメントアウトしてます)
function Test($Scripts, $N)
{
# 各スクリプトの並びをシャッフルしてN回試行しその平均タイムを取得
$Scripts * $N | Sort-Object { random } |
select Name, @{ name = "Time"; expression = { (Measure-Command $_.Script).TotalMilliseconds } } |
group Name |
select Name, @{ name="AverageTime"; expression = { ($_.Group | measure Time -Average).Average } } |
Sort-Object AverageTime |
ft Name,@{ label = "AverageTime(msec)"; expression = { $_.AverageTime -as "int" } } -AutoSize
}
#Import-Module ".\looptest.dll" -Cmdlet "Get-Twice"
$src = @"
using System;
using System.Linq;
public class LoopTest
{
public static int[] Twice(object[] array)
{
return array.Select(x => 2 * (int)x).ToArray();
}
}
"@
Add-Type -TypeDefinition $src -Language CSharp
filter TwiceFilter { 2 * $_ }
function TwiceFunction { process{ 2 * $_ } }
function TwiceAdvance { param([Parameter(ValueFromPipeline)][int]$v) process { 2 * $v } }
$foreachScripts = @(
[pscustomobject]@{
Name = "ドットソース演算子(.)"
Script = { $array | . { process{ 2 * $_ } } }
}
[pscustomobject]@{
Name = "呼び出し演算子(&)"
Script = { $array | & { process{ 2 * $_ } } }
}
[pscustomobject]@{
Name = "filter"
Script = { $array | TwiceFilter }
}
[pscustomobject]@{
Name = "関数"
Script = { $array | TwiceFunction }
}
[pscustomobject]@{
Name = "高度な関数"
Script = { $array | TwiceAdvance }
}
[pscustomobject]@{
Name = "ForEach-Object"
Script = { $array | ForEach-Object { 2 * $_ } }
}
[pscustomobject]@{
Name = "ForEachメソッド"
Script = { $array.ForEach({ 2 * $_ }) }
}
[pscustomobject]@{
Name = "foreach文"
Script = { foreach ($i in $array) { 2 * $i } }
}
[pscustomobject]@{
Name = "for文"
Script = { for ($i = 0; $i -lt $array.Length; $i++) { 2 * $array[$i] } }
}
[pscustomobject]@{
Name = "while文"
Script = { $i = 0; while($i -lt $array.Length) { 2 * $array[$i]; $i++ } }
}
[pscustomobject]@{
Name = "switch文"
Script = { switch($array) { default { 2 * $_ } } }
}
[pscustomobject]@{
Name = "Select(LINQ)"
Script = { [System.Linq.Enumerable]::Select($array, [Func[object,int]]{ param($i) 2 * $i }) }
}
[pscustomobject]@{
Name = "C# (Add-Type)"
Script = { [LoopTest]::Twice($array) }
}
#[pscustomobject]@{
# Name = "コマンドレット(Binary Module)"
# Script = { $array | Get-Twice }
#}
)
$array = 1..500000
$n = 10 # ループ1種類辺り計測回数
"*"*60
"各種ループでサイズ$($array.Count)のint型配列の各要素に2をかけた値を取得するテスト"
"${n}回ずつ計測して平均タイムをミリ秒で取得"
"*"*60
Test $foreachScripts $n
Powershell5.1で行いました。
PS > $PSVersionTable
Name Value
---- -----
PSVersion 5.1.17763.134
PSEdition Desktop
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0...}
BuildVersion 10.0.17763.134
CLRVersion 4.0.30319.42000
WSManStackVersion 3.0
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1
Where-Object
と各フィルタ処理の比較も行ったので後で投稿しようと思います。