Webを眺めていたらこんな記事が目に止まった。
Selecting Property order - Richard Siddaway's Blog
簡単に解釈すると、「Select-Object」するときに、プロパティをすべて表示させたいのだが一部のプロパティだけを先頭に持ってくるにはどうしたら良いかという話のようだ。
全項目指定すれば順番に並べることは可能だがそれは面倒。
- ps | select name , *
こんな指定が出来れば良いのだが、プロパティの重複でエラーになる。
そこで以下のような関数を書いてみた。という趣旨だと思う。
- function Select-Order {
- [CmdletBinding()]
- param (
- [parameter(Position=0,
- ValueFromPipeline=$true)]
- $InputObject,
- [string[]]$firstprop
- )
- PROCESS {
- $proplist = $firstprop
- $Inputobject | Get-Member -MemberType Property |
- foreach {
- if ($firstprop -notcontains $_.Name){
- $proplist += $_.Name
- }
- }
- $InputObject | select -Property $proplist
- }}
確かにキー項目のようなものだけ先頭にしたいときはあるなぁと思い、プロファイルに入れることにした。
その際、リスト形式で表示されては見づらいのでGridViewに出力するよう変更しておこうと深く考えずに20行目を以下のように変更し、
- $InputObject | select -Property $proplist | ogv
これを実行したらとんでもない目にあった。(1行1GridでGridがウィルスのように湧いて出てきた)
- ps | Select-Order name
という訳で、纏めてGridVew出力に変更したのが以下のサンプルである。
- function slo {
- [CmdletBinding()]
- param (
- [parameter(ValueFromPipeline=$true)]
- $InputObject,
- [parameter(Position=0)]
- [string[]]$firstprop
- )
- BEGIN{$out = @()}
- PROCESS {
- $proplist = $firstprop
- $Inputobject | Get-Member -MemberType Property |
- foreach {
- if ($firstprop -notcontains $_.Name){
- $proplist += $_.Name
- }
- }
- $out += $InputObject | select -Property $proplist
- }
- END{
- $out | ogv
- }
- }
- ps | slo name
これにて一件落着という所ではあるのだが、なんとなくすっきりしない。
そもそも、このPROCESSブロックは必要なのだろうか。
最初にやったように、単純にOut-GridViewにパイプするととんでもないことになるのは、このPROCESSブロックのせいである。
Out-GridViewを使わないにしても入力パイプライン1件ずつに対してSelectするのは無駄ではないだろうか。
出力にについても、本来以下のように1件ずつ出力されているのがマージされて結果的に纏まって表示されているに過ぎない。
そこで、PROCESSブロックを使わずに$input自動変数で入力を纏めて取ってくるよう変更し比較してみようと思う。
- function slo {
- [CmdletBinding()]
- param (
- [parameter(ValueFromPipeline=$true)]
- $InputObject,
- [parameter(Position=0)]
- [string[]]$firstprop
- )
- $proplist = $firstprop
- $input | Get-Member -MemberType Property |
- foreach {
- if ($firstprop -notcontains $_.Name){
- $proplist += $_.Name
- }
- }
- $input | select -Property $proplist | ogv
- }
- ps | slo name
こんな感じで良いだろうと思ったのだが、これではうまくいかない。
12行目の$inputは参照できるのだが、不思議なことに18行目では$inputが参照できないのである。(何も返ってこない)
実は$inputは「ArrayListEnumeratorSimple」という型になっていて入力パイプラインのコレクションに対する「列挙」なのである。
なので、一度最後まで読みだすとそのままではもう一度読みだすことが出来ない。
そこで、18行目の前に、
- $input.Reset()
を追加してあげる必要があるのだ。
最後に、実際両者の性能はどうなるのか比較してみた。(Grid出力ではなく単純なSelectで比較)
- function slo {
- [CmdletBinding()]
- param (
- [parameter(ValueFromPipeline=$true)]
- $InputObject,
- [parameter(Position=0)]
- [string[]]$firstprop
- )
- PROCESS {
- $proplist = $firstprop
- $Inputobject | Get-Member -MemberType Property |
- foreach {
- if ($firstprop -notcontains $_.Name){
- $proplist += $_.Name
- }
- }
- $InputObject | select -Property $proplist
- }
- }
- measure-command{
- (ps) * 5 | slo name
- }
- function slo {
- [CmdletBinding()]
- param (
- [parameter(ValueFromPipeline=$true)]
- $InputObject,
- [parameter(Position=0)]
- [string[]]$firstprop
- )
- $proplist = $firstprop
- $input | Get-Member -MemberType Property |
- foreach {
- if ($firstprop -notcontains $_.Name){
- $proplist += $_.Name
- }
- }
- $input.Reset()
- $input | select -Property $proplist
- }
- measure-command{
- (ps) * 5 | slo name
- }
結果を見ると、予想以上に$inputを使用したサンプルのほうが速いのが判る。
基本的に私はPowerShellで速度を論じるのは好きではないが、これだけの速度差があると看過できない問題に思える。
牟田口さんからコメントを頂きちょっと見直しました。(いつもありがとうございます)
まさにご指摘の通り、本質的に時間がかかっている原因はプロパティの解析を毎回行なっているところにありますね。
時間が違いすぎておかしいなとは思っていたのですが、安易にそのまま掲載してしまいました。(^^;
ご指摘内容を踏まえて、当初の目的であるグリッドビュー出力のサンプルを再掲します。
- function Select-Order2
- {
- [CmdletBinding()]
- param (
- [parameter(ValueFromPipeline=$true)]
- [object[]]$InputObject,
- [parameter(Position=0)]
- [string[]]$firstprop
- )
- begin
- {
- $proplist = @()
- $out = @()
- }
- process
- {
- if($proplist.length -eq 0)
- {
- $proplist = $firstprop
- $Inputobject | Get-Member -MemberType Property |
- foreach {
- if($proplist -notcontains $_.Name)
- {
- $proplist += $_.Name
- }
- }
- }
- $out += $InputObject | select-object -property $proplist
- }
- end
- {
- $out | ogv
- }
- }
- measure-command{
- (ps) * 5 | Select-Order2 name
- }
- function slo {
- [CmdletBinding()]
- param (
- [parameter(ValueFromPipeline=$true)]
- $InputObject,
- [parameter(Position=0)]
- [string[]]$firstprop
- )
- $proplist = $firstprop
- $input | Get-Member -MemberType Property |
- foreach {
- if ($firstprop -notcontains $_.Name){
- $proplist += $_.Name
- }
- }
- $input.Reset()
- $input | select -Property $proplist | ogv
- }
- measure-command{
- (ps) * 5 | slo name
- }
これで両者ほぼ速度的には似たようなものになりました。
ただし、ターンアラウンドタイムは変わらないのですが、レスポンスタイムについては$inputを使ったほうが圧倒的に速いようです。
これは、まさに牟田口さんご指摘のパイプラインの威力なのでしょう。
また、もうひとつご指摘いただいたパラメータを直接指定できなくなることにつきましては私も懸念しておりました。
パラメータ指定の時とパイプライン入力の判定をして処理を振り分けないといけなくなるのでスマートじゃないんですよね。
パイプライン入力を受け付けるにも関わらず、ValueFromPipelineなプロパティが存在しないぞとか思ったりして・・・。
対象要素数が多くなると指数関数的に処理速度が遅くなるやつは注意しないと、バッチとして動かしていたのにある日を境に急激に処理が遅くなる、という現象を起こすので注意が要りますよね。
返信削除今回のスクリプトは元が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
補足お疲れ様です。
返信削除今回の関数(Out-GridViewするバージョン)では関数が値を返さないので必ずパイプラインの末端にくるので、$inputを使うパターンでもありかな、という気はします。
processを使っても結局はOut-GridViewするのは最後の一回だけで、そこは$inputを使う場合と変わらないですしね。本来ならprocessの中で逐次out-gridviewに項目を送りたいところですが、それはできなさそうですね。
次の記事に書かれている通り、Out-*系のコマンドレットは自作関数に組み込まないのがよいのかもですね。
牟田口さんにそう言っていただけると、とりあえず今の方針で良いのかなと思えてきました。
返信削除今回は本当に勉強になりました。
ありがとうございます。
いえいえ、私も今回の件では勉強になりました。
返信削除process{}に書けば配列の処理はすべてうまくいくと思い込んでいたので、他のコマンドレットを組み合わせようとしたときの落とし穴に気づいてなかったですので。
ありがとうございます。