tech.guitarrapc.cóm

Technical updates

PowerShell の配列表現と生成処理時間

面白い記事があります。

blog.shibata.tech

PowerShell において配列生成は 言語仕様上にある通りカンマ演算子 , によって表現されるものであり、ASTでも満たされている。しかし、そこに言及がなく ()$()@() で生成しているような表現を見かけるけど実は違うんだよ。ということが説明されています。とはいえ、@() で囲むことに意味はあるので注意なのですが。

tech.guitarrapc.com

さて、結果としてみるとどの表現でも配列が生成されます。が、AST を見てもそれぞれ違うことから「表現可能な方法が複数ある場合にどれを使うのがいいのか」を考えてみましょう。

目次

PowerShell の構文木 AST を見る

もし PowerShell の AST が見たい場合は、ShowPSAst でモジュールを入れておくといいでしょう。

# 今のユーザーにのみ導入する
Install-Module ShowPSAst -Scope CurrentUser

github.com

配列の生成

今回は、ベンチマークでは単純に配列評価の時間を測定したいため、配列の生成は事前に文字列を起こしておきましょう。

以下のようにすると配列が生成できます。

gist.github.com

これで2種類の配列文字列が取得できたので準備ok です。

1,2,3,4,5,....

1
,2
,3
,4
,5
....

ベンチマークの測定対象

それではベンチマークを測ってみましょう。

測定対象として選んだのは記事にあった表現とその派生です。

  1. ArrayLiteralAst : カンマ演算子, による配列の生成 = 最速の予定
    • 1,2,3
  2. ParenExpressionAst + ArrayLiteralAst : () で カンマ演算子 , による配列の生成のラップ
    • (1,2,3)
  3. ArrayExpressionAst + ArrayLiteralAst : @() でカンマ演算子 , による配列の生成のラップ
    • @(1,2,3)
  4. SubExpressionAst + ArrayLiteralAst : $() でカンマ演算子 , による配列の生成のラップ
    • $(1,2,3)
  5. (ArrayExpression + ArrayLiteralAst) * PipelineOutput : @() でカンマ演算子 , による配列の生成のラップした結果をパイプラインでマップ
    • @(1,2,3) | % {$_}
  6. Constraints + ArrayLiteralAst : @() で生成した中身を前置カンマにしました
@(
1
,2
,3)

ベンチマーク結果

1000回実行したっけの平均/最大/最小を見ます。単位は ms です。

PowerShell でのベンチマークは、今回簡易に Measure-Command を用いました。

Code Target Count Average Maximum Minimum
1,2,3 ArrayLiteralAst 1000 6.72703 76.897 1.109
(1,2,3) ParenExpressionAst + ArrayLiteralAst 1000 6.70472 77.452 1.0702
@(1,2,3) ArrayExpressionAst + ArrayLiteralAst 1000 7.020254 185.7868 1.0828
$(1,2,3) SubExpressionAst + ArrayLiteralAst 1000 7.59060 85.0647 1.4674
前述参照 (ArrayExpression + ArrayLiteralAst) * PipelineOutput 1000 75.666 234.0299 52.0301
前述参照 Constraints + ArrayLiteralAst 1000 8.67313 195.6095 6.1331

いかがでしょうか? 予想通りですか?

ArrayLiteralAst

さすがに Average / Maximum / Minimum のいずれにおいても安定して最速です。

ArrayLiteralAst だけの場合、次のAST評価となっています。

# AST  : {1,2,3} | Show-Ast
# Eval : ScriptBlockAst > NameBlockAst > PipelineAst > CommandExpressionAst > [ArrayLiteralAst] > ConstantExpressionAst(s)

ParenExpressionAst + ArrayLiteralAst

こちらも ArrayLiteralAst のみと比較して、ParenExpressionAst + ArrayLiteralAst では、() で括った分一段要素が増えます。一方で実行速度にはほとんど差がなく、() は評価の軽い要素であるのが明確です。

# AST  : {(1,2,3)} | Show-Ast
# Eval : ScriptBlockAst > NameBlockAst > PipelineAst > CommandExpressionAst > [ParenExpressionAst] > PipelineAst > CommandExpressionAst > [ArrayLiteralAst] > ConstantExpressionAst(s)

ArrayExpressionAst + ArrayLiteralAst

Maximum 測定誤差がでたと考えられます。次のAST評価となっています。 ArrayExpressionAst + StatementBlock + CommandExpressionAst が増えていることからもそこそこ評価が増えてきました。が誤差レベルですね。

# AST  : {@(1,2,3)} | Show-Ast
# Eval : ScriptBlockAst > NameBlockAst > PipelineAst > CommandExpressionAst > [ArrayExpressionAst] > StatementBlockAst > PipelineAst > CommandExpressionAst > ArrayLiteralAst > ConstantExpressionAst(s)

SubExpressionAst + ArrayLiteralAst

こちらは、Minimum が少し大きいですが同様に誤差でしょう。

部分式は多用するのですが、AST評価を見ても [SubExpressionAst] > StatementBlockAst > PipelineAst > CommandExpressionAst となっており、ArrayExpressionAst とだいたい同様ですね。こちらも気にしなくてよさそうです。

# AST  : {$(1,2,3)} | Show-Ast
# Eval : ScriptBlockAst > NameBlockAst > PipelineAst > CommandExpressionAst > [SubExpressionAst] > StatementBlockAst > PipelineAst > CommandExpressionAst > ArrayLiteralAst > ConstantExpressionAst(s)

(ArrayExpression + ArrayLiteralAst) * PipelineOutput

原因は明らかで パイプラインです。ASTを見ても明らかに要素数が多くなることが分かります。パイプラインほんと重いんですよね。配列を生成するためにこの利用は避けましょう。

# AST  : {@(1,2,3) | % {$_}} | Show-Ast
# Eval : ScriptBlockAst > NameBlockAst > PipelineAst > CommandExpressionAst > [ArrayExpressionAst] > StatementBlockAst > PipelineAst > CommandExpressionAst > ArrayLiteralAst > ConstantExpressionAst(s)
#                                                             | > CommandAst 
#                                                                         | > StringConstantExpressionAst
#                                                                         | > ScriptBlockExpressionAst > ScriptBlockAst > NamedBlockAst > PipelineAst > CommandExpressionAst > VariableExpressionAst

Constraints + ArrayLiteralAst

最初の要素 1 のみ 速やかに ConstantExpressionAst として評価されています。しかし後続は前置のカンマによってシングル要素の配列 とAST評価されてしまいArrayLiteralAst とついています。AST評価を見てみると明らかですね。

# AST  : {@(
# 1
# ,2
# ,3)
# } | Show-Ast
# Eval : ScriptBlockAst > NameBlockAst > PipelineAst > CommandExpressionAst > [ArrayExpressionAst] > StatementBlockAst > PipelineAst > CommandExpressionAst > [ConstantExpressionAst]
#                                                                                                                    | > PipelineAst(s) > CommandExpressionAst > [ArrayLiteralAst] > ConstantExpressionAst
#                                                                                                                    | > PipelineAst(s) > CommandExpressionAst > [ArrayLiteralAst] > ConstantExpressionAst

まとめ

特に制約がない時に書くなら、すなおに , でくくるのみにするか () で括るのが良さそうです。

$a = 1,2,3
$b = (1,2,3)

String Interporation のような文字列埋め込みに使う 表現も悪くはなさそうです。

$c = "$(1,2,3)" // "1 2 3" となる

良く紹介される形も明らかな齟齬はなさそうです。

$d = @(1,2,3)

ただしパイプライン、お前はだめだ。

$e = @(1,2,3) | % {$_}

ベンチマークコード全体

コードを置いておきます。参考になれば幸いです。

https://gist.github.com/guitarrapc/8a89dc9438673871a71649ab8315e0e8