tech.guitarrapc.cóm

Technical updates

PowerShell V4 の コレクションフィルタリングについてまとめてみる

PowerShell V4 で待望のメソッド構文での .Where().ForEach()が追加されました。

これまでにも、パイプラインを介した Where-Object によるコレクションのフィルタリングはありましたが、メソッド構文が導入されたのは大きな一歩です。

しかしPowerShell におけるコレクションフィルタリングについてまとまった記事は少ないようです。そこで PowerShell V1,2,3,4 における Where構文を初めとする 各種コレクションフィルタリングに関してまとめてみましょう。

また、PowerShell V4 で追加された Where()メソッド構文での WhereOperatorSelectionMode についても紹介します。

コレクションフィルタリング一覧

PowerShell において、コレクションをフィルタするために以下の方法が使われます。

利用可能なバージョンとその特徴を見てみましょう。

ver 概要 長所 短所
V1- Where-Object{} ・ どのPSバージョンでも実行可能
・ オブジェクトをパイプランを通してストリーム処理することでメモリが最小限で済む
・ スクリプトブロックではどのような記述も可能
・実行速度が遅い
V3- Where-Object省略記法 ・ Where フィルタよりわずかに高速
・ オブジェクトをパイプランを通してストリーム処理することでメモリが最小限で済む
・ スクリプトブロックでは一つのプロパティ指定するのみに限定されており複雑な記法は負荷
実行速度がそこまで早くない
・ PowerShell 3.0以降でのみ動作する
V4- .Whereメソッド記法 ・ パイプラインを通したWhere構文より2倍程度高速
・ スクリプトブロックではどのような記述も可能
・ オブジェクトをパイプランを通してストリーム処理できないため、処理はメモリに収まる範囲に限定される
・ PowerShell 4.0以降でのみ動作する
V1- filter構文内でフィルタリング ・ どのPSバージョンでも実行可能
・ オブジェクトをパイプランを通してストリーム処理することでメモリが最小限で済む
・ スクリプトブロックではどのような記述も可能
・ Where-ObjectやWhereメソッド構文の利用より高速
・ロジック定義箇所を参照しないよ処理内容がわからないため可読性が高くない
V1- foreach構文内でフィルタリング ・ どのPSバージョンでも実行可能
・ ロープ内でどのような記述も可能
・ 最速
・オブジェクトをパイプランを通してストリーム処理できないため、処理はメモリに収まる範囲に限定される

各種コレクションフィルタリングによる速度

ではサンプルコードを使って、各種コレクションフィルタリングでどのような速度差があるか見てみましょう。

# -- stopwatch function -- #

function Measure-Stopwatch{

    [CmdletBinding()]
    param(
    [parameter(Mandatory=$true)]
    [ScriptBlock]$Command,
    [switch]$Days,
    [switch]$Hours,
    [switch]$Minutes,
    [switch]$Seconds,
    [switch]$Milliseconds
    )

    $sw = New-Object System.Diagnostics.StopWatch
    
    # Start Stopwatch
    $sw.Start()

    #TargetCommand to measure
    $command.Invoke()
    
    # Stop Stopwatch
    $sw.Stop()
    
    #Show Result
    switch ($true){
        $Days {$sw.Elapsed.TotalDays}
        $Hours {$sw.Elapsed.TotalHours}
        $Minutes {$sw.Elapsed.TotalMinutes}
        $Seconds {$sw.Elapsed.TotalSeconds}
        $Milliseconds {$sw.Elapsed.TotalMilliseconds}
        default {$sw.Elapsed}
    }

    #Reset Result
    $sw.Reset()
    
}


# -- define repeat count -- #

$repeat = 1000

# -- test ScriptBlock -- #

$ps1Where = {
    foreach ($r in 1..$repeat)
    {
        Get-Process | where { $_.Name -eq "powershell_ise" } > $null
    }
}

$ps3Where = {
    foreach ($r in 1..$repeat)
    {
        Get-Process | where Verb -eq "powershell_ise" > $null
    }
}

$ps4Where = {
    foreach ($r in 1..$repeat)
    {
        (Get-Process).Where({$_.Name -eq "powershell_ise"}) > $null
    }
}

$Filter= {
    filter filterCommand {if ($_.Name -eq "powershell_ise"){$_}}
    foreach ($r in 1..$repeat)
    {
        Get-Process | filterCommand > $null
    }
}

$Foreach = {
    foreach ($r in 1..$repeat)
    {
        foreach ($process in Get-Process)
        {
            if ($process.Name -eq "powershell_ise")
            {
                $process > $null
            }
        }
    }
}

実行してみましょう。

# -- start main -- #

Write-Host "running ps1- Where-Object cluese" -ForegroundColor DarkGray
$PS1Millisec = Measure-Stopwatch -Command $ps1Where -Milliseconds

Write-Host "running ps3- Where-Object Simplfied syntax" -ForegroundColor DarkGray
$PS3Millisec = Measure-Stopwatch -Command $ps3Where -Milliseconds

Write-Host "running ps4 Where method collection" -ForegroundColor DarkGray
$PS4Millisec = Measure-Stopwatch -Command $ps4Where -Milliseconds

Write-Host "running Filter collection test" -ForegroundColor DarkGray
$FilterMillisec = Measure-Stopwatch -Command $Filter -Milliseconds

Write-Host "running Foreach conditional collection" -ForegroundColor DarkGray
$ForeachMillisec = Measure-Stopwatch -Command $Foreach -Milliseconds


# -- show result -- #

New-Object PSObject -Property ([ordered]@{
    "ps1[ms]"     = $ps1Millisec
    "ps3[ms]"     = $ps3Millisec
    "ps4[ms]"     = $ps4Millisec
    "filter[ms]"  = $FilterMillisec
    "foreach[ms]" = $ForeachMillisec
}) | Format-List

実行速度の結果です。 PowerShell V4で導入された Whereメソッド構文が、パイプラインを介したWhere-Objectに比較してかなりの高速化が図られていることがわかります。

ps1[ms]     : 10810.8184
ps3[ms]     :  8796.8583
ps4[ms]     :  6419.6145
filter[ms]  :  5709.9363
foreach[ms] :  5071.7525

また、Get-ProcessではなくGet-Commandにした場合、foreachを利用したコレクションフィルタリングは1000回のループとなるとうまくメモリに収まらず効率的に動作しません。 が、パイプラインやWhereメソッド構文であれば問題なく動作したことも特筆しておきます。 (もちろん パイプラインを利用した Where-Objectよりも高速に動作します。)

PowerShell V4 で導入された WhereOperatorSelectionMode について

これについては、まだ海外でもまとまった記事がほとんどありませんのでいい機会です。

WhereOperatorSelectionMode の一覧は、以下のmsdnにあります。

WhereOperatorSelectionMode Enumeration

Enum一覧を取得するには、以下のコードを実行します。

[Enum]::GetNames([System.Management.Automation.WhereOperatorSelectionMode])

結果です。

Default
First
Last
SkipUntil
Until
Split

これらのオペレータは、.Where({ScriptBlock},"Enum",Value) のように第二引数、第三引数にいれます。*1

では、それぞれのオペレータについてについてみていきましょう。

通常

偶数を抽出します。

(1..10).Where({$_ % 2 -eq 0})

結果です。

2
4
6
8
10

Default

何も指定していない状態と同一です。

(1..10).Where({$_ % 2 -eq 0},"Default")

結果です。

2
4
6
8
10

First

ScriptBlockの実行結果の最初から数えて、第三引数で指定した数を出力します。

(1..10).Where({$_ % 2 -eq 0},"first",2)
2
4

Last

ScriptBlockの実行結果の最後から数えて、第三引数で指定した数を出力します。

(1..10).Where({$_ % 2 -eq 0},"last",2)
8
10

SkipUntil

ScriptBlockの実行結果が満たされるまでスキップし、第三引数で指定した数だけ出力します。

以下の場合は、2~10までのコレクションで、-1して3になるまでスキップし1つだけ出力なので 4のみ出力します。

(2..10).Where({$_ - 1 -eq 3},"SkipUntil",1)
4

以下なら余が1の数値までスキップし、2つ出力です。

(2..10).Where({$_ % 2 -eq 1},"SkipUntil",2)
3
4

$null を指定するとコレクションに操作を加えず、そのまま実行します。

(2..10).Where($null,"SkipUntil",2)
4
5
6
7
8
9
10

Until

ScriptBlockの実行結果が満たされるまで第三引数で指定した数だけ出力し、残りはスキップします。

(1..10).Where($null,"Until",5)
1
2
3
4
5

Split

指定したスクリプトブロック結果でコレクションを分割し、それぞれを第三引数で指定した数だけ配列要素として出力し、配列の残りは別の配列要素に格納します。

奇数を出力後に、偶数を出力しています。

(1..10).where({$_%2},"Split")
1
3
5
7
9
2
4
6
8
10

スクリプトブロックで満たされた偶数は、出力されたコレクションの初めの要素に格納されています。

(1..10).where({$_%2},"Split")[0]
1
3
5
7
9

3を指定しているので、インデックス0の出力要素は3つです。

PS> (1..10).where({$_ % 2},"Split","3")[0]
1
3
5

残りの要素は、[1]に格納されています。

(1..10).where({$_ % 2},"Split","3")[1]
2
4
6
7
8
9
10

まとめ

Whereメソッド構文による速度面での恩恵は大きく、ワンライナーでパイプラインを介さずにかけるのは大いに役立ちます。

また、WhereOperatorSelectionModeによる処理は非常に強力で、さらにパイプラインを容易に減らしコード記述が楽になります。

ぜひぜひ使って行けばいいと思います。*2

*1:.ForEach()には利用できない

*2:PowerShell4 4.0でしか動作しませんが