Help us understand the problem. What is going on with this article?

Powershell 各ループの速度比較

More than 1 year has passed since last update.

各ループの動作速度がどの程度なのかをあまり把握せずになんとなく選択している状態だったので一度測ってみようと思い計測してみました。

計測

1から50万までの数字が入った配列を入力して、各要素に2をかけた数を出力するシンプルなコードで行いました。

array
$array = 1..500000

ForEach-Objectの場合

ForEach-Object
$array | ForEach-Object { 2 * $_ }

結果

foreach.png

感想

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から使うことはないでしょう。

結論

今回の結果を見て今後スクリプト書く時の指針というか自分なりの結論です。

  • &.foreachfilterの速度は優秀なので積極的に使う。

  • さらに速度が必要な場合はC#のコードをAdd-Typeして使う。

  • for文やwhile文はそんなに速くないので、ループカウンタを+1して回すような単純な処理の場合はforeach文等で代用した方がよい。

  • ForEach-Objectはシンプルで簡便に書けるのがメリットだが、とても遅いので大量のデータを入力するような場合には使うべきではない。

  • 高度な関数のパイプライン入力は速くないので、単純なフィルタリングなら高度な関数にするべきではない。

今回計測したループ処理

便宜上ループって書いてますがforeach的な処理です。

ForEach-Object

ForEach-Object
$array | ForEach-Object { 2 * $_ }

foreach文

foreach_statement
foreach ($i in $array) { 2 * $i }

呼び出し演算子 (&)

&
$array | & { process { 2 * $_ } }

ドットソース演算子 (.)

.
$array | . { process { 2 * $_ } }

関数

function
function TwiceFunction { process { 2 * $_ } }
$array | TwiceFunction

filter

filter
filter TwiceFilter { 2 * $_ }
$array | TwiceFilter

高度な関数

advanced_function
TwiceAdvance { param([Parameter(ValueFromPipeline)][int]$v) process { 2 * $v } }
$array | TwiceAdvance

ForEachメソッド

ForEach_method
$array.ForEach({ 2 * $_ })

for文

for_statement
for ($i = 0; $i -lt $array.Length; $i++) { 2 * $array[$i] }

while文

while_statement
$i = 0; while ($i -lt $array.Length) { 2 * $array[$i]; $i++ }

switch文

switch_statement
switch ($array) { default { 2 * $_ } }

Select (LINQ)

Select(LINQ)
[System.Linq.Enumerable]::Select($array, [Func[object,int]]{ param($i) 2 * $i })

C# (Add-Type)

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)

C#_Cmdlet
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);
        }
    }
}
Cmdlet(BinaryModule)
Import-Module "./LoopTest.dll" -Cmdlet "Get-Twice"
$array | Get-Twice

今回使ったコード全て

コピペでそのまま実行できます。(バイナリモジュールの部分はコメントアウトしてます)
Measure
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で行いました。

PSVersionTable
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と各フィルタ処理の比較も行ったので後で投稿しようと思います。

acuo
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away