PowerShellの書き方とパフォーマンスについて、パイプラインと構造化どちらが良いの?

自分の場合は、PowerShellを大きなデータ処理などに使ったことが無いため、実は意識したことは少ないのですが、少しの書き方の違いでPowerShellのパフォーマンスは大きく変わるようです。

内容はこちらの動画を参考にして書いています。
Windows PowerShell Best Practices and Patterns: Time to Get Serious | TechEd North America 2014 | Channel 9


知らなかったこともあったのでまとめておこうと思います。

PipingとStructured

PowerShellは大きく分けるとコマンドをパイプで繋いでいく書き方(Piping)と、$始まりの変数に格納してfor文などで処理していく構造化プログラミング的な書き方(Structured)があります。一時変数を取る構造化プログラミング的な書き方と比較して、パイプでつなぐ方法はストリーム渡しになるのでメモリの消費が少なくなります。このようなことから2つの書き方は「一般的に」以下のような特徴があるようです。
 

メモリ使用量 スピード
Piping 少ない 遅い
Structured 多い 早い

試してみる

実際に計測してみるのですが、PowerShellで処理のスピードを計りたい場合は「Measure-Command」のコマンドレットが便利です。使い方は以下のとおり。

Measure-Command {計測したい処理内容}


では、計測してみます。今回は単純な例を使ってみます。

#Pipingの例
Measure-Command{
    Get-Process | Write-Output
}

#Structuredの例
Measure-Command{
    $procs = Get-Process
    Write-Output $procs
}
  • 結果
一回目 二回目 三回目
Piping 11.5399 4.7889 16.8277
Structured 2.4643 1.8701 2.2841

数字の単位はMillisecondsです。毎回実行時間が大きく異るのは気になりますが、やはりPipingの方が時間がかかるようです。


例外はたくさんある

ただ、注意したいのは、上記のPipingとStructuredのパフォーマンスの考え方はあくまでも一般論であるということです。実際には処理によって例外はいくらでもあるようです。

例えば、例外として以下のものが挙げられています。

#処理-1
Measure-Command{
    $procs = Get-Process
    foreach($proc in $procs){
        $total = $proc.VM + $proc.PM
        Write-Output "$($proc.Name) $($total)"
    }
}

#処理-2
Measure-Command{
    Get-Process | ForEach-Object {
        Write-Output "$($_.Name) $($_.VM + $_PM)"
    }
}

#処理-3
Measure-Command{
    Get-Process | Select-Object -Property Name,@{n='Total'; e={$_.VM + $_.PM}}
}
  • 結果
一回目 二回目 三回目
処理-1 47.5274 46.2833 25.2704
処理-2 33.7952 38.7329 26.1907
処理-3 29.8404 25.6604 15.1976


処理-3の書き方が最も早いことがわかります。処理内容の単純さから考えると少しだけトリッキーな気もしますがPipingであることに間違いありません。処理内容と書き方によってはPipingの方が早くなる場合もあるという例でした。

チューニングの方法も様々

このように一般論はあるものの、書き方次第で処理スピードが大きく変わるのがPowerShellのようです。

例えば、単純にfile.txtというファイルの中身を一行ずつ取得して出力する以下の処理の場合を2パターン見てみます。

#処理-1
$content = Get-Content .\file.txt
ForEach($line in $content){
    Write-Output -input $line
}

#処理-2
$sr = New-Object -Type System.IO.StreamReader -Arg .\file.txt
while($sr.Peek() -ge 0){
    $line = $sr.ReadLine()
    Write-Output -input $line
}

通常の書き方である処理-1とPowerShellより低位の.Netレベルで書いている処理-2があります。私の環境ではfile.txtのサイズが小さいうちは処理-1の方が早く、file.txtのサイズが大きくなると処理-2の方が早くなりました。

まとめ

PowerShellにはPiping、Structured、.Netクラス利用など様々な書き方があり、それぞれを組み合わせることができます。一般論はあるものの、組み合わせが多いためケース毎に最適な方法は変わってきそうです。特にパフォーマンスが重要になる箇所はMeasure-Commandで実際に計測して最適な書き方を探すと良いようです。