2025-10-19

F# で Cmdlet を書いてる pt.77

krymtkts/pocof を開発した。

とりあえず query を正規表現で分割してから組み立てるのでなくて自前の parser で組み立てるのに書き直し始めたところ。 Parser Combinator にするっつってたが、アレはやめた。 どうしても抽象度が上がって関数の数が増えることによる overhead で、さほど速くならないのがわかったからだ。 結局 pocof のような小規模な構文だと、小さな関数にまとめた方が圧倒的に速いのがわかってそうした。速さは正義だ。

まず既存のコードに不足しているテストを足して benchmark を足して、そんでからまず単純に書き換えた版を作った。 #372

benchmark は以下の通り。

before(regular expression)

Method QueryCount Mean Error StdDev Median Ratio RatioSD Gen0 Allocated Alloc Ratio
prepareNormalQuery 1 1.455 us 0.0291 us 0.0638 us 1.429 us 1.00 0.06 0.6828 2.79 KB 1.00
preparePropertyQuery 1 1.658 us 0.0327 us 0.0573 us 1.652 us 1.14 0.06 0.7210 2.95 KB 1.06
prepareNormalQuery 3 2.872 us 0.0574 us 0.1353 us 2.830 us 1.00 0.07 1.3275 5.45 KB 1.00
preparePropertyQuery 3 3.310 us 0.0650 us 0.1067 us 3.330 us 1.16 0.06 1.4267 5.84 KB 1.07
prepareNormalQuery 5 4.249 us 0.0847 us 0.1484 us 4.224 us 1.00 0.05 1.9989 8.19 KB 1.00
preparePropertyQuery 5 5.064 us 0.0920 us 0.2187 us 5.011 us 1.19 0.07 2.1439 8.78 KB 1.07
prepareNormalQuery 7 5.615 us 0.1081 us 0.1585 us 5.626 us 1.00 0.04 2.6550 10.84 KB 1.00
preparePropertyQuery 7 6.571 us 0.1270 us 0.1901 us 6.597 us 1.17 0.05 2.8381 11.6 KB 1.07

after(tokenizer + parser)

Method QueryCount Mean Error StdDev Ratio RatioSD Gen0 Allocated Alloc Ratio
prepareNormalQuery 1 1.273 us 0.0241 us 0.0237 us 1.00 0.03 0.6828 2.79 KB 1.00
preparePropertyQuery 1 1.334 us 0.0261 us 0.0428 us 1.05 0.04 0.7210 2.95 KB 1.06
prepareNormalQuery 3 2.446 us 0.0485 us 0.0539 us 1.00 0.03 1.3313 5.45 KB 1.00
preparePropertyQuery 3 2.640 us 0.0527 us 0.0881 us 1.08 0.04 1.4267 5.84 KB 1.07
prepareNormalQuery 5 3.735 us 0.0730 us 0.1179 us 1.00 0.04 1.9989 8.19 KB 1.00
preparePropertyQuery 5 3.968 us 0.0778 us 0.1115 us 1.06 0.04 2.1439 8.78 KB 1.07
prepareNormalQuery 7 4.959 us 0.0979 us 0.1005 us 1.00 0.03 2.6550 10.84 KB 1.00
preparePropertyQuery 7 5.168 us 0.0983 us 0.1244 us 1.04 0.03 2.8381 11.6 KB 1.07

after(parser index)

Method QueryCount Mean Error StdDev Ratio RatioSD Gen0 Allocated Alloc Ratio
prepareNormalQuery 1 1.208 us 0.0238 us 0.0491 us 1.00 0.06 0.6523 2.66 KB 1.00
preparePropertyQuery 1 1.239 us 0.0245 us 0.0490 us 1.03 0.06 0.6866 2.8 KB 1.05
prepareNormalQuery 3 2.358 us 0.0467 us 0.1054 us 1.00 0.06 1.2970 5.3 KB 1.00
preparePropertyQuery 3 2.517 us 0.0492 us 0.0875 us 1.07 0.06 1.3657 5.58 KB 1.05
prepareNormalQuery 5 3.524 us 0.0691 us 0.1155 us 1.00 0.05 1.9455 7.95 KB 1.00
preparePropertyQuery 5 3.753 us 0.0741 us 0.1530 us 1.07 0.06 2.0370 8.34 KB 1.05
prepareNormalQuery 7 4.680 us 0.0932 us 0.1861 us 1.00 0.06 2.5864 10.59 KB 1.00
preparePropertyQuery 7 4.999 us 0.0990 us 0.1810 us 1.07 0.06 2.7237 11.13 KB 1.05

before(regular expression) が元で、 after(parser index) が最終形。 after(tokenizer + parser) は中間の状態で、元の tokenizer と parser が別れてたやつ。これも統合したほうが速くなった。 結果的に、普通の query と property query 共に元の 2 割ほど実行時間を短縮できた。

さらにここから、差分 compile を導入するだとか、コードクォートで filtering の predicate を構築するのが別れてるのを統合するだとかができたら、更に良さそう。

今回書いてみて思ったのが、効率が良い自前の parser を書くのは、実際のところ .NET Standard 2.0 を target framework にしてると厳しい。 benchmark を取った訳では無いが、多分 ReadOnlySpan を使えばまだ追求できるんじゃないかな。 やっぱ文字列操作が重いからな。 でも .NET Standard 2.0 では使えないから仕方がないね。

将来的に multi target で build して配布するというのもアリかも知れないが、ややこしいしまだ本気では考えてない。 でも Windows PowerShell がいつなくなるかも決まってないから、 multi target が今後性能を追求していくとしたら一番現実的かな。

query の parser を自前で書くのに着手する前は、気持ちを整えるため、ムダに割当されていた List の除去だったり諸々の改善と、その過程で見つけた旧来からの bug を修正していた。 結構しょぼい bug が残ってたので、前にも触れたがテストケースを更新するのも重要になってきたな~。 #370 #371

結構書き換わってる気がするので、もうそろそろ新しいの出して自分の日々の仕事でも使おうかな。