面白い記事があります。
PowerShell において配列生成は 言語仕様上にある通りカンマ演算子 ,
によって表現されるものであり、ASTでも満たされている。しかし、そこに言及がなく ()
や $()
や @()
で生成しているような表現を見かけるけど実は違うんだよ。ということが説明されています。とはいえ、@()
で囲むことに意味はあるので注意なのですが。
さて、結果としてみるとどの表現でも配列が生成されます。が、AST を見てもそれぞれ違うことから「表現可能な方法が複数ある場合にどれを使うのがいいのか」を考えてみましょう。
目次
PowerShell の構文木 AST を見る
もし PowerShell の AST が見たい場合は、ShowPSAst でモジュールを入れておくといいでしょう。
# 今のユーザーにのみ導入する Install-Module ShowPSAst -Scope CurrentUser
配列の生成
今回は、ベンチマークでは単純に配列評価の時間を測定したいため、配列の生成は事前に文字列を起こしておきましょう。
以下のようにすると配列が生成できます。
これで2種類の配列文字列が取得できたので準備ok です。
1,2,3,4,5,....
と
1 ,2 ,3 ,4 ,5 ....
ベンチマークの測定対象
それではベンチマークを測ってみましょう。
測定対象として選んだのは記事にあった表現とその派生です。
- ArrayLiteralAst : カンマ演算子
,
による配列の生成 = 最速の予定1,2,3
- ParenExpressionAst + ArrayLiteralAst :
()
で カンマ演算子,
による配列の生成のラップ(1,2,3)
- ArrayExpressionAst + ArrayLiteralAst :
@()
でカンマ演算子,
による配列の生成のラップ@(1,2,3)
- SubExpressionAst + ArrayLiteralAst :
$()
でカンマ演算子,
による配列の生成のラップ$(1,2,3)
- (ArrayExpression + ArrayLiteralAst) * PipelineOutput :
@()
でカンマ演算子,
による配列の生成のラップした結果をパイプラインでマップ@(1,2,3) | % {$_}
- 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