文字列連結の速度を測る
はじめに
PowerShell 7.5 が出て少々経ちましたが、ふとリリースノートを見ると Array += が高速化されているとあるではないですか! List.Add より速い、と。
今まで文字列の連結に StringBuilder や List.Add が速いとは知りつつも、記述が長くなるのを嫌って使ってこなかった人(私です)には朗報!
参考: https://zenn.dev/haretokidoki/articles/bfc40f861a480d
ってことで少し調べました。List.Add より Array+= が速くなったというのは公式に乗っているので省略してそれ以外を調べます。
テストの方法とコード
文字列の連結操作を大量に行うのは、姓 + 名 → 氏名のようにオブジェクトの複数のプロパティの連結を大量に行うケースと、大量のデータを一つの文字列に纏めて出力するケースの2つがあります。このパターンの違いは、後者のケースでは += のような処理方式が性能面で不利になることが予想できること。テストも分けて行う必要があるでしょう。
文字列連結の方法
前出の、はれときどきZennさんによれば文字列連結の方法は13種もあるそうですなのですが、現実的ではない手法もありますし、StringBuilder とかかったるくて使わない(おい)ので、お手軽コーディング用に厳選wします。
$s = $s1 + $s2
$s1 += $s2
$s = "$s1$s2"
$s = '{0}{1}' -f $s1,$s2
-
$s = [String]::Join('', $s1,$s2)
# お手軽とは言い難いけどめっちゃ速いなら使わないこともなくってよ $s = ($s1,$s2) -join('')
テストは、ケース1として予め設定してある文字列 $s1 と $s2 を連結するというのをループで行うものと、ケース2として予め設定してある文字列の配列を連結して1つの文字列にする、という2パターン。
ケース1結果
比較的短い2つの文字列2つを連結する処理を5万回繰り返して測定します。計測は5回行い平均を使います。
# PowerShell 7.5
Code Average Ratio
---- ------- -----
4: $str = "{0}{1}" -f $s1,$s2 0.3830 1.00
1: $str = $s1 + $s2 0.4003 1.05
3: $str = "$s1$s2" 0.4031 1.05
2: $s1 += $s2 0.4331 1.13
6: $str = $s1,$s2 -join("") 0.4336 1.13
5: $str = [string]::Join("", $s1, $s2) 0.6579 1.72
微妙は差はありますが、大差ないので一番わかりやすい $s1 + $s2 一択でしょう。'{0}' -f は事故りやすいですし。
ちなみに "{0}{1}" -f はシングルクォートを使って '{0}{1}' -f にすると極めて微妙な差ですが速くなります。
# PowerShell 5.1
Code Average Ratio
---- ------- -----
1: $str = $s1 + $s2 0.7980 1
2: $s1 += $s2 1.0930 1.369663
5: $str = [string]::Join("", $s1, $s2) 1.0997 1.378094
4: $str = "{0}{1}" -f $s1,$s2 1.1068 1.387012
3: $str = "$s1$s2" 1.1243 1.409009
6: $str = $s1,$s2 -join("") 1.3518 1.694012
5.1 だとちょっと変わりますね。 += 速っ、と思いがちですが、実測値は 1.09 で Pwsh 7.5 で一番遅かった [String]::Join() の0.66 よりも遅いという...。一番早い $s1 + $s2 でも 0.8 で Pwsh 7.5 に追いつきません。
ケース2 結果
ケース2は大量の文字列を一つにするパターンです。これはデータ量によって結果も変わるかもしれないということで1000個の文字列を連結して 138K ほどの文字列にするパターンと、1万個を連結して 852K ほどの文字列にするパターンを試してみます。
# PowerShell 7.5, 1000個の文字列連結
Code Average Ratio
---- ------- -----
5*: $Str = [string]::Join("", $SourceStrings) 0.0009 1.00
6*: $Str = $SourceStrings -join("") 0.0011 1.32
ex1: $Str = [string]$SourceStrings 0.0015 1.74
6: $SourceStrings |%{ $tmp += ,$_ }; $Str = $tmp -join("") 0.0224 26.11
3: $SourceStrings |%{ $Str = "$Str$_" } 0.0238 27.70
5: $SourceStrings |%{ $tmp += ,$_ }; $Str = [String]::Join("", $tmp) 0.0246 28.66
1: $SourceStrings |%{ $Str = $Str + $_ } 0.0280 32.58
2: $SourceStrings |%{ $Str += $_ } 0.0308 35.93
4: $SourceStrings |%{ $Str = "{0}{1}" -f $Str,$_ } 0.0312 36.32
5* と 6* は予め用意したテストデータ(SourceStrings[])をそのまま連結するパターンで最速なのですが、実際にはこのように文字列に何も手を加えないで単に連結というのはそうそうなく、何かしらの処理をしながらになるはずなので単なる参考値としています。外部から受け取ったデータを変更しないで使う場合でも、受け取ってためておくという処理が必要になるので実戦用の 5、6 はその処理を計測時間に含めています。
ex1 としている [string]$SourceStrings
はキャストによって[string[]] を [string] にしているわけですが空白文字が文字列の間に挿入されて出力結果も変わってくるのでここれも参考値扱い。空白入れて連結するのが目的でも Join(' ') のほうが速いしわかりやすいしで、トリッキーなキャストの出番はないでしょう。(と言いつつ一番記述が短いのでデバッグ出力用に使ってるけどな)
$Str = "$Str$_"
が意外に速くて驚きなのですが、ここはやはり Join 一択でしょうか。 -join を使うか [string]::join() かは趣味で選ぶ領域ですが、個人的には記述が短い -join() で決まり。
# PowerShell 7.5, 1万個の文字列連結
Code Average Ratio
---- ------- -----
5*: $Str = [string]::Join("", $SourceStrings) 0.0022 1.00
ex1: $Str = [string]$SourceStrings 0.0038 1.76
6*: $Str = $SourceStrings -join("") 0.0039 1.79
5: $SourceStrings |%{ $tmp += ,$_ }; $Str = [String]::Join("", $tmp) 0.7122 328.19
6: $SourceStrings |%{ $tmp += ,$_ }; $Str = $tmp -join("") 0.7466 344.05
2: $SourceStrings |%{ $Str += $_ } 2.8055 1292.85
1: $SourceStrings |%{ $Str = $Str + $_ } 2.8797 1327.06
4: $SourceStrings |%{ $Str = "{0}{1}" -f $Str,$_ } 3.2506 1497.96
3: $SourceStrings |%{ $Str = "$Str$_" } 3.3158 1528.01
意外な速さの $Str = "$Str$_"
もさすがに足される側になる $Str が長くなると沈没します。変わって += がかなり頑張りますが、やはり Array+= からの join には敵わない。はい次。
# PowerShell 5.1 1000個連結
Code Average Ratio
---- ------- -----
5*: $Str = [string]::Join("", $SourceStrings) 0.0004 1
ex1: $Str = [string]$SourceStrings 0.0013 3.037717
6*: $Str = $SourceStrings -join("") 0.0023 5.593619
6: $SourceStrings |%{ $tmp += ,$_ }; $Str = $tmp -join("") 0.0198 47.47446
5: $SourceStrings |%{ $tmp += ,$_ }; $Str = [String]::Join("", $tmp) 0.0198 47.66747
2: $SourceStrings |%{ $Str += $_ } 0.0800 192.1454
1: $SourceStrings |%{ $Str = $Str + $_ } 0.0812 195.0744
4: $SourceStrings |%{ $Str = "{0}{1}" -f $Str,$_ } 0.1591 382.1544
3: $SourceStrings |%{ $Str = "$Str$_" } 0.1607 386.0157
# PowerSHell 5.1 1万個連結
Code Average Ratio
---- ------- -----
5*: $Str = [string]::Join("", $SourceStrings) 0.0036 1
6*: $Str = $SourceStrings -join("") 0.0128 3.579055
ex1: $Str = [string]$SourceStrings 0.0146 4.080982
5: $SourceStrings |%{ $tmp += ,$_ }; $Str = [String]::Join("", $tmp) 1.4894 416.6254
6: $SourceStrings |%{ $tmp += ,$_ }; $Str = $tmp -join("") 1.5072 421.5986
2: $SourceStrings |%{ $Str += $_ } 5.1359 1436.684
1: $SourceStrings |%{ $Str = $Str + $_ } 5.1803 1449.089
3: $SourceStrings |%{ $Str = "$Str$_" } 10.5657 2955.559
4: $SourceStrings |%{ $Str = "{0}{1}" -f $Str,$_ } 10.6456 2977.916
PowerShell 5.1 でも傾向は変わりませんね。全体的に遅くなるのも一緒。5.1 は忘れてもいいかな。?: 無くてツライし。
結論
最終的な結論ですが、単に文字列繋ぐだけなら $s1 + $s2
で速度面でも見やすさ面でも問題なし。
データの値だけではなく名称も表示したいとき、例えば
code> $items
Name Qty
---- ---
Apple 10
Orange 5
このようなデータをわかりやすく表示するときに "$a $b" とか '{0}' -f はありでしょう。コードの見やすさも + より上な気がします。
code> $items |%{ "品物:$($_.Name) 個数:$($_.Qty)" }
品物:Apple 個数:10
品物:Orange 個数:5
大量の文字列を1つにするようなケースは join で。記述が短い -join() がおすすめで、色々なパターンに対応できるという観点からも -join [string[]] ではなく、[string[]] -join(デリミタ) 形式で使うのが良いですね。
つまらない結論なのですが、普通に使うやつが速度的にも速いというのは良かった。
Discussion
はじめまして、akiGAMEBOYです。たまたま見ていたら私の記事を参考にして頂けていたのでビックリしました。ご紹介もして頂いてありがとうございます!
PowerShell 7.5 になって、そのような変更があったのですね。まだ触っていないので参考になりました。
本文にある通り
StringBuilder
は本当に使いにくいですよね 😂簡単が売りのスクリプト言語との相性は……
akiGAMEBOYさん、コメントありがとうございます。文字列連結の記事は幾つもあるのですが一番網羅的に書いてあるので前から参照しておりました:)
StringBuilder とか List.Add とか、どうなのと思う反面、Split/Join-Path より [IO.Path] の関数のほうが使いやすかったり、悩ましいですね。