2011年12月25日日曜日

◆$inputの使い方

Webを眺めていたらこんな記事が目に止まった。
Selecting Property order - Richard Siddaway's Blog

簡単に解釈すると、「Select-Object」するときに、プロパティをすべて表示させたいのだが一部のプロパティだけを先頭に持ってくるにはどうしたら良いかという話のようだ。
全項目指定すれば順番に並べることは可能だがそれは面倒。

  1. ps | select name , *

こんな指定が出来れば良いのだが、プロパティの重複でエラーになる。
そこで以下のような関数を書いてみた。という趣旨だと思う。

一部のプロパティ順を指定する関数
  1. function Select-Order {           
  2. [CmdletBinding()]           
  3. param (           
  4. [parameter(Position=0,           
  5.   ValueFromPipeline=$true)]           
  6.   $InputObject,            
  7.             [string[]]$firstprop
  8. )           
  9.           
  10. PROCESS {           
  11.             $proplist = $firstprop
  12.           
  13. $Inputobject | Get-Member -MemberType Property |           
  14. foreach {           
  15. if ($firstprop -notcontains $_.Name){           
  16.   $proplist += $_.Name           
  17. }           
  18. }           
  19.           
  20.             $InputObject | select -Property $proplist
  21. }}

確かにキー項目のようなものだけ先頭にしたいときはあるなぁと思い、プロファイルに入れることにした。
その際、リスト形式で表示されては見づらいのでGridViewに出力するよう変更しておこうと深く考えずに20行目を以下のように変更し、

  1. $InputObject | select -Property $proplist | ogv

これを実行したらとんでもない目にあった。(1行1GridでGridがウィルスのように湧いて出てきた)

  1. ps | Select-Order name

という訳で、纏めてGridVew出力に変更したのが以下のサンプルである。

Select結果を纏めてGridViewに
  1. function slo {           
  2. [CmdletBinding()]           
  3. param (           
  4.    [parameter(ValueFromPipeline=$true)]           
  5.     $InputObject,            
  6.    [parameter(Position=0)]
  7.                [string[]]$firstprop
  8. )           
  9.           
  10. BEGIN{$out = @()}
  11. PROCESS {           
  12.                $proplist = $firstprop
  13.               
  14.    $Inputobject | Get-Member -MemberType Property |           
  15.    foreach {           
  16.      if ($firstprop -notcontains $_.Name){           
  17.       $proplist += $_.Name           
  18.      }           
  19.    }           
  20.             $out += $InputObject | select -Property $proplist
  21. }
  22. END{
  23.    $out | ogv
  24. }
  25. }
  26.  
  27. ps | slo name

これにて一件落着という所ではあるのだが、なんとなくすっきりしない。

そもそも、このPROCESSブロックは必要なのだろうか。
最初にやったように、単純にOut-GridViewにパイプするととんでもないことになるのは、このPROCESSブロックのせいである。
Out-GridViewを使わないにしても入力パイプライン1件ずつに対してSelectするのは無駄ではないだろうか。
出力にについても、本来以下のように1件ずつ出力されているのがマージされて結果的に纏まって表示されているに過ぎない。
image

そこで、PROCESSブロックを使わずに$input自動変数で入力を纏めて取ってくるよう変更し比較してみようと思う。

$inputを使ったサンプル
  1. function slo {           
  2. [CmdletBinding()]           
  3. param (           
  4.    [parameter(ValueFromPipeline=$true)]           
  5.     $InputObject,            
  6.    [parameter(Position=0)]
  7.                [string[]]$firstprop
  8. )           
  9.           
  10.             $proplist = $firstprop
  11.             
  12. $input | Get-Member -MemberType Property |           
  13. foreach {           
  14.    if ($firstprop -notcontains $_.Name){           
  15.     $proplist += $_.Name           
  16.    }           
  17. }
  18.      $input | select -Property $proplist | ogv
  19. }
  20.  
  21. ps | slo name

こんな感じで良いだろうと思ったのだが、これではうまくいかない。
12行目の$inputは参照できるのだが、不思議なことに18行目では$inputが参照できないのである。(何も返ってこない)

実は$inputは「ArrayListEnumeratorSimple」という型になっていて入力パイプラインのコレクションに対する「列挙」なのである。
なので、一度最後まで読みだすとそのままではもう一度読みだすことが出来ない。
そこで、18行目の前に、

  1. $input.Reset()

を追加してあげる必要があるのだ。

最後に、実際両者の性能はどうなるのか比較してみた。(Grid出力ではなく単純なSelectで比較)

PROCESSブロックを使ったサンプル
  1. function slo {           
  2. [CmdletBinding()]           
  3.     param (           
  4.         [parameter(ValueFromPipeline=$true)]           
  5.        $InputObject,            
  6.         [parameter(Position=0)]
  7.                     [string[]]$firstprop
  8.     )           
  9.           
  10.     PROCESS {           
  11.                     $proplist = $firstprop
  12.                    
  13.         $Inputobject | Get-Member -MemberType Property |           
  14.         foreach {           
  15.             if ($firstprop -notcontains $_.Name){           
  16.              $proplist += $_.Name           
  17.             }           
  18.         }           
  19.                  $InputObject | select -Property $proplist
  20.     }
  21. }
  22.  
  23. measure-command{
  24.     (ps) * 5 | slo name
  25. }

image

$inputを使ったサンプル
  1. function slo {           
  2. [CmdletBinding()]           
  3.     param (           
  4.         [parameter(ValueFromPipeline=$true)]           
  5.        $InputObject,            
  6.         [parameter(Position=0)]
  7.                     [string[]]$firstprop
  8.     )           
  9.           
  10.                 $proplist = $firstprop
  11.                
  12.     $input | Get-Member -MemberType Property |           
  13.     foreach {           
  14.         if ($firstprop -notcontains $_.Name){           
  15.          $proplist += $_.Name           
  16.         }           
  17.     }
  18.     $input.Reset()
  19.          $input | select -Property $proplist
  20. }
  21.  
  22. measure-command{
  23.     (ps) * 5 | slo name
  24. }

image

結果を見ると、予想以上に$inputを使用したサンプルのほうが速いのが判る。

基本的に私はPowerShellで速度を論じるのは好きではないが、これだけの速度差があると看過できない問題に思える。


牟田口さんからコメントを頂きちょっと見直しました。(いつもありがとうございます)

まさにご指摘の通り、本質的に時間がかかっている原因はプロパティの解析を毎回行なっているところにありますね。
時間が違いすぎておかしいなとは思っていたのですが、安易にそのまま掲載してしまいました。(^^;

ご指摘内容を踏まえて、当初の目的であるグリッドビュー出力のサンプルを再掲します。

PROCESSブロックを使ったサンプル
  1. function Select-Order2
  2.             {
  3.    [CmdletBinding()]            
  4.    param (            
  5.        [parameter(ValueFromPipeline=$true)]
  6.        [object[]]$InputObject,   
  7.        [parameter(Position=0)]          
  8.        [string[]]$firstprop
  9.    )            
  10.    begin
  11.    {
  12.       $proplist = @()
  13.        $out = @()
  14.    }
  15.               
  16.    process
  17.    {
  18.        if($proplist.length -eq 0)
  19.        {              
  20.            $proplist = $firstprop
  21.            $Inputobject | Get-Member -MemberType Property |
  22.            foreach {
  23.                if($proplist -notcontains $_.Name)
  24.                {
  25.                    $proplist += $_.Name
  26.                }
  27.            }
  28.        }
  29.        $out += $InputObject | select-object -property $proplist
  30.    }        
  31.    
  32.    end
  33.    {
  34.        $out | ogv
  35.    }
  36. }
  37. measure-command{
  38.    (ps) * 5 | Select-Order2 name
  39. }
$inputを使ったサンプル
  1. function slo {             
  2. [CmdletBinding()]             
  3.    param (             
  4.        [parameter(ValueFromPipeline=$true)]             
  5.        $InputObject,              
  6.        [parameter(Position=0)]
  7.        [string[]]$firstprop
  8.    )             
  9.            
  10.    $proplist = $firstprop
  11.                 
  12.    $input | Get-Member -MemberType Property |             
  13.    foreach {             
  14.        if ($firstprop -notcontains $_.Name){             
  15.         $proplist += $_.Name             
  16.        }             
  17.    }
  18.    $input.Reset()
  19.    $input | select -Property $proplist | ogv
  20. }
  21. measure-command{
  22.    (ps) * 5 | slo name
  23. }

これで両者ほぼ速度的には似たようなものになりました。

ただし、ターンアラウンドタイムは変わらないのですが、レスポンスタイムについては$inputを使ったほうが圧倒的に速いようです。
これは、まさに牟田口さんご指摘のパイプラインの威力なのでしょう。

また、もうひとつご指摘いただいたパラメータを直接指定できなくなることにつきましては私も懸念しておりました。
パラメータ指定の時とパイプライン入力の判定をして処理を振り分けないといけなくなるのでスマートじゃないんですよね。
パイプライン入力を受け付けるにも関わらず、ValueFromPipelineなプロパティが存在しないぞとか思ったりして・・・。

4 件のコメント:

  1. 対象要素数が多くなると指数関数的に処理速度が遅くなるやつは注意しないと、バッチとして動かしていたのにある日を境に急激に処理が遅くなる、という現象を起こすので注意が要りますよね。

    今回のスクリプトは元がO(n^3)な処理になってるので遅くなってたんだと思います。

    修正後のスクリプトはO(n^2)になったので処理は速くなっていますが二つ問題が…

    一つはパラメータの$InputObjectが使われてないのでパラメータで渡す処理ができなくなってます。つまり
    slo name -inputobject (ps)
    ができなくなってます。

    もう一つはprocess{}を使ってないのでパイプラインでコマンドを連結した際、slo関数直前までのコマンドがすべて終了してからでないとslo関数以降が処理されなくなってしまいます。

    1..10|&{process{start-sleep -sec 1;$_}}|&{process{$_}}
    なら1秒ごとに値が表示されますが
    1..10|&{process{start-sleep -sec 1;$_}}|&{$input}
    だと10秒経たないと値が一つも表示されないというのと同じ問題ですね。

    なのでパイプラインで配列を渡す関数は、process{}で列挙を処理してやるのが鉄則かと思います。

    今回の元のスクリプトはprocess{}の中で不必要なループ(Get-Memberしてる部分)があるせいでO(n^3)になってしまっているので、そこを取り除けばO(n^2)になります。

    Select-Objectの部分はprocess{}の中に入れても
    $inputを処理しても同じくO(n^2)の処理なのでここを変更しても全体の速度向上に寄与しません。

    これらの点を考慮して修正したのでよかったら見ていただければと思います。
    速度的にはminminnanaさんバージョンとほぼ変わらないと思います。
    http://winscript.jp/powershell/files/Select-Order2.txt

    返信削除
  2. 補足お疲れ様です。
    今回の関数(Out-GridViewするバージョン)では関数が値を返さないので必ずパイプラインの末端にくるので、$inputを使うパターンでもありかな、という気はします。

    processを使っても結局はOut-GridViewするのは最後の一回だけで、そこは$inputを使う場合と変わらないですしね。本来ならprocessの中で逐次out-gridviewに項目を送りたいところですが、それはできなさそうですね。

    次の記事に書かれている通り、Out-*系のコマンドレットは自作関数に組み込まないのがよいのかもですね。

    返信削除
  3. 牟田口さんにそう言っていただけると、とりあえず今の方針で良いのかなと思えてきました。
    今回は本当に勉強になりました。
    ありがとうございます。

    返信削除
  4. いえいえ、私も今回の件では勉強になりました。
    process{}に書けば配列の処理はすべてうまくいくと思い込んでいたので、他のコマンドレットを組み合わせようとしたときの落とし穴に気づいてなかったですので。
    ありがとうございます。

    返信削除