tech.guitarrapc.cóm

Technical updates

PowerShell で オブジェクトの数を計測するんです?

オブジェクトの数を数える時によく使われるものといえば?

Measure-Objectでしょうか? Measure-Objectでしょうか?

今回はさらっとちょっとだけ見てみましょう。

個数のカウント

よく使うのはどれでしょうか?

  • .Lengthプロパティ?
  • .Countプロパティ?
  • `Measure-Object`` コマンドレット?
  • System.Linq.EnumerableSystem.Linq.Enumerableメソッド?

.Lengthプロパティは .NET通りなのでいいでしょう。

ここではPowerShellで奇妙に感じる.CountプロパティとCmdletである.Countを見てみます。

.Countプロパティって気持ち悪いけど便利

PowerShellのオブジェクトは、そこかしこで .Countプロパティを使って個数を数えることができます。*1 image

そう、たとえ配列でなくてもです。

気になるのが文字列System.Stringに対してですが、「インテリセンス上はLengthしかでず」これは文字長を取得します。*2

image

しかし、インテリセンスになくとも.Countが使える不思議。これは.Countであれば、必ず1になります。空文字.Countであってもです。

https://gist.github.com/guitarrapc/1e3498cec68800cd12e4

まぁ、便利は便利です。ICollectionインタフェースの.Countプロパティ同様に取得コストがほぼないので。

しかし、配列でもはいのに要素数が.Countで取れるのは気持ち悪いことこの上ない。

.Countプロパティの利用は罠がある

PowerShell 6.1からPSCustomObjectのCountプロパティが機能するようになっています。

https://blog.shibata.tech/entry/2018/10/07/172334

以下の内容はPowerShell 6.0以下のバージョンにおいて該当します。

大体のオブジェクトに対して.Countで個数が取れるのですが、数少ない例外が.Countです。

この型に対しては、.Countプロパティでの個数取得できません。

image

となった時に、.Lengthプロパティが使えるかというと同様にnullが返ってきます。

これを回避するには、いったん配列@()で包んでしまいます。

https://gist.github.com/guitarrapc/c75b35bb51747f2aac44

Measure-Object を使って個数を数える

ここでようやく、Measure-Object Cmdletが出てきます。

プロパティを使って取得できるなら、可能な限りそっちを使うのがふつーなのですが、取れないなら数えるしかないと。

よくあるパターンでPSCustomObject型を生成して個数を数えるとこうでしょうか?

https://gist.github.com/guitarrapc/daaf304d5aa8e2773aa0

Measure-Object の罠

正確には、パイプライン|の罠です。

PowerShellのパイプラインは、基本的にはオブジェクトを1つずつ渡します。*3

ということは? パイプラインに1000000オブジェクト渡すと、1000000オブジェクト渡しきるのを待たないといけないということです。これはひどい。

そのため、以下のように書いた日には18秒ぐらいぽけ~(())と待つことになります。やめましょう。

(1000000..1 | measure).Count

個数カウントの方法とベンチマーク

楽をしたいので、C# で良くやるようにPowerShellにも持ち込みます。

PowerShellの長所でもあり短所であるパイプラインです。コストがパイプラインにかかっているので、避ければいいのです。

ということで、System.Linq.Enumerableを使えばいいでしょう。それぞれのベンチをとってみます。

https://gist.github.com/guitarrapc/acd1b4a5a1a1f3d7fdbe

方法 1回目 2 回目 3回目 平均(ms)
.Count 261.2029 155.8287 145.4331 187.488
Measure-Object 19268.5511 19558.6135 19934.1167 19587.093
Linq.Enumerable.Count() 405.0513 357.8473 389.6365 384.18

当然の結果ですが、想定通りですね。

Min / Max の取得

この件がもっと影響を及ぼすのが、Min / Maxです。

Measure-Objectはとても便利なCmdletで、Min/Max/Average/Countをプロパティを指定して計測できます。

しかし、パイプライン越しの利用が主体になるため、オブジェクト個数に応じて時間がかかります。(1-1000程度ならmsで完了するのでいいのですが)

そういう意味では、System.Linq.Enumerableは、PowerShellからも使いやすいので是非使えばいいでしょう。

https://gist.github.com/guitarrapc/4f6146d48adef782cf3a

Min

方法 1回目 2 回目 3回目 平均(ms)
Measure-Object 22628.2811 22705.8116 23010.3391 22781.477
Linq.Enumerable.Min() 451.5155 431.6266 566.3141 483.152

Max

方法 1回目 2 回目 3回目 平均(ms)
Measure-Object 23102.14 22624.6367 22535.7175 22754.164
Linq.Enumerable.Max() 460.4299 548.7585 390.1181 466.435

Average

方法 1回目 2 回目 3回目 平均(ms)
Measure-Object 20982.5092 21166.3688 20576.0699 20908.315
Linq.Enumerable.Agerage() 382.4254 370.6433 360.0512 371.03

まとめ

.Countプロパティが使える時は素直に使いましょう。

Measure-Objectは量が十分抑えられる時には便利ですね。

Linq.Enumerableくぁわいい。

*1:実態は .Lengthプロパティです

*2:この場合は4が取得できます

*3:より正確にはひと塊ずつです。デフォルト1塊り、1オブジェクトなので一見1つずつになります。