VBAでString型変数に何度も文字列を結合する場合の注意点
Dim str As String
str = str & "hogehoge..."
何気なくやるこんな処理。大量に繰り返すと、意外な落とし穴があります。C言語でメモリーの管理に慣れている人なら、気が付くでしょうが、経験がなければ一生気が付かないかもしれません。
いま、1行当たり200byte程度、1つ20KB程度のテキストファイルが1年分365個あって、それらを一つのテキストファイルにまとめるとします。String型の変数"data"を用意し、1つずつテキストファイルを開き、1行ずつ読み取ってdataの後ろに次々に結合していきます。
Sub hoge(data$)
'<中略>
Do While EOF(1) = False
Dim line_$
Line Input #1, line_
fields_ = Split(line_, vbTab)
Dim i&
For i = LBound(fields_) to Ubound(fields_)
Dim str$: str = fields_(i)
'<何らかの処理>
data = data & "," & str
Next
Loop
Close #1
End Sub
そして、最後の最後で出力先ファイルに書き込みます。ExcelVBAで良く言われる「セルへの書き込みの回数は少ない方が良い」に倣えば、これって効率的なはずですよね。
さて、最後の365個目のテキストファイルをdataに結合するころには、何秒くらい経っていると思いますか。
Windows10 Core i5/ RAM16GBのラップトップPCで1時間40分かかりました。しかも、おかしなことに最初のファイル数十件はあっという間に終わり、数が増えるごとに指数関数的に時間がかかっているようでした。
文字列の結合以外の処理もたくさん含まれているのですが、処理速度に与える影響は小さかったです。最初は結合以外の処理に時間がかかっているものだと思い込んんでいたのですが、検証した結果、真の原因は、文字列の結合でした。
そこで、変数dataのサイズがあまり大きくならないよう、1つファイルを読み込んで処理が終わるごとに変数dataの中身を出力先のファイルに追記モードで書き込み、変数dataをクリアして次のファイルに移るようにしたところ、全く同じ処理がものの10秒足らずで終わるようになりました。
何故文字列の結合に時間がかかるのか
文字列の結合に時間がかかる原因を調べるうちに次のようなことが分かりました。
アンパサンド(&)を使ってString型変数に文字列を結合すると、当然、文字列の長さが長くなります。文字列の長さが長くなると、それまでそのString型変数に割り当てられていたメモリー領域では文字列が入りきれなくなるので、文字列の大きさに合わせたメモリー領域が新たにString型変数に割り当てられます。次に、それまで使っていたメモリー領域から新しく大きくなったメモリー領域に値(文字列)がコピーされます。これらの処理にどの程度時間がかかるのか、簡単なコードで実証したところ、文字列のサイズがある一定の閾値を超えた時に劇的に遅くなるのが分かりました。
Sub Example()
Dim str As String
Dim i As Long
For i = 1 To 50000
str = str & "a"
Next
End Sub
<検証結果>
回数 処理時間(ms)
50,000 0.072
100,000 0.299
250,000 2.083
500,000 8.023
もし、最終的な文字列の長さが予め分かっているなら、String型変数を固定長で宣言し、続きは前回の最後の位置から書き込むようにすれば、 & を使わずに済むのでメモリー領域の再割り当てが無い分処理速度は速くなります。
Dim str As String*1000
但し、調べたところ 固定長で宣言する場合は 65526文字が限度のようです。これでは少なすぎてあまり実用的ではないかもしれません。