Powershell 各ループの速度比較

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

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

```powershell:array
$array = 1..500000
```
#### ForEach-Objectの場合

```powershell:ForEach-Object
$array | ForEach-Object { 2 * $_ }
```



## 結果



![foreach.png](https://qiita-image-store.s3.amazonaws.com/0/253358/a868452e-5db5-7c40-d54b-e471c1a394aa.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から使うことはないでしょう。



## 結論

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

* `&`、`.`、`foreach`、`filter`の速度は優秀なので積極的に使う。

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

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

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

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



## 今回計測したループ処理

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

### ForEach-Object

```powershell:ForEach-Object
$array | ForEach-Object { 2 * $_ }
```
###  foreach文

```powershell:foreach_statement
foreach ($i in $array) { 2 * $i }
```
###  呼び出し演算子 (&)

```powershell:&
$array | & { process { 2 * $_ } }
```
###  ドットソース演算子 (.)

```powershell:.
$array | . { process { 2 * $_ } }
```
###  関数

```powershell:function
function TwiceFunction { process { 2 * $_ } }
$array | TwiceFunction
```
###  filter

```powershell:filter
filter TwiceFilter { 2 * $_ }
$array | TwiceFilter
```
###  高度な関数

```powershell:advanced_function
TwiceAdvance { param([Parameter(ValueFromPipeline)][int]$v) process { 2 * $v } }
$array | TwiceAdvance
```
###  ForEachメソッド

```powershell:ForEach_method
$array.ForEach({ 2 * $_ })
```
###  for文

```powershell:for_statement
for ($i = 0; $i -lt $array.Length; $i++) { 2 * $array[$i] }
```
###  while文

```powershell:while_statement
$i = 0; while ($i -lt $array.Length) { 2 * $array[$i]; $i++ }
```
###  switch文

```powershell:switch_statement
switch ($array) { default { 2 * $_ } }
```
###  Select (LINQ)

```powershell:Select(LINQ)
[System.Linq.Enumerable]::Select($array, [Func[object,int]]{ param($i) 2 * $i })
```
###  C# (Add-Type)

```powershell: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#: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);
        }
    }
}
```

```powershell:Cmdlet(BinaryModule)
Import-Module "./LoopTest.dll" -Cmdlet "Get-Twice"
$array | Get-Twice
```



## 今回使ったコード全て

<details><summary>コピペでそのまま実行できます。（バイナリモジュールの部分はコメントアウトしてます）</summary><div>

```powershell: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
```

</div></details>

Powershell5.1で行いました。

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