#31 文字列結合処理を考える

結論
長い文字列の結合処理においては、その回数および結合される側の変数の長さが短いほど、処理時間を短くできる。さらに、結合するのではなく、Mid$関数を使うことによって任意の位置に文字列を当てはめる方策を講じることによって、処理時間をほとんどゼロにすることも可能。

メモリ構造などを頭に入れながらプログラミングしなければならないような他のプログラミング言語に比べて、BASICは何の気兼ねなく文字列結合などの文字列処理を行うことができます。一般的に使われる文字列(String)型の「可変長文字列」では、一応最大2GBという制限はありますが、実質的にはその長さをまったく気にせずプログラミングできるといってもよいでしょう。

しかしその一方で、表面的な使いやすさの陰で、文字列結合においてはかなり面倒な内部処理が行われています。例えば、ある文字列変数に"ABC"という文字が格納されているとします。そして、その後ろに"DEF"という文字列を結合したいとします。コードの上では、"ABC" & "DEF"というように、単に2つの文字列を&(アンパサンド)で結合するだけです。しかし内部的には、"ABC"という文字列が格納されているメモリ上の領域の次に"DEF"が単純に追加書き込みされるわけではありません。まず、十分な空き領域のある別の領域に"ABC"がコピーされ、その後ろに"DEF"が追加されます。可変長文字列の場合、どんな長さの文字列が追加されるか分からないため、このような処理が行われるのです。もし、最初に"ABC"が格納されているメモリ領域の次に"DEF"を書き込んだとしたら、別の変数の領域と競合してしまう可能性があるからです。

このような理由から、VBAによる文字列結合処理では、メモリ操作上のオーバーヘッドが発生している可能性が考えられます。そこでここでは、いつくかの文字列結合処理のコードを使って、最適な方法を探してみたいと思います。


まず、上記にあるような一般的なコーディングによって文字列結合処理を行ってみます。

次の処理では、最初は長さゼロであった文字列変数 strData に対して、"A"という1文字を次々後ろに追加結合していきます。最終的には"A"という文字が6万字連なる変数となります。しかし、あえてここではそれらを3回に分けて、2万字ずつ追加していくようにしてあります。そしてそれぞれのポイントで時間測定を行っています。これは、文字列の追加に伴って、現在のメモリ領域から別の領域にいったんコピーされるという内部処理を考えたとき、そのコピー元の文字列長が長ければ長いほど、処理時間がかかるのではないかという想定からです。1回目の追加では、ループの最後で19999文字がコピーされますが、3回目のループの最後では59999文字がコピーされることになります。その文字数の違いがどれくらい影響を与えるかを調べようというものです。


Sub StrTest1()

  Dim strData As String
  Dim iintLoop As Integer

  '文字列変数を空にする
  strData = ""
  Debug.Print Timer

  '1回目の2万字を追加
  For iintLoop = 1 To 20000
    strData = strData & "A"
  Next iintLoop
  Debug.Print Timer

  '2回目の2万字を追加
  For iintLoop = 1 To 20000
    strData = strData & "A"
  Next iintLoop
  Debug.Print Timer

  '3回目の2万字を追加
  For iintLoop = 1 To 20000
    strData = strData & "A"
  Next iintLoop
  Debug.Print Timer

End Sub


このコードの実行結果を下表に示します。
処理 実行時間
1回目の追加 0.93 秒
2回目の追加 7.86 秒
3回目の追加 18.45 秒
合計 27.24 秒
想像通りの結果となりました。何万回文字を追加しようと、それぞれに追加される文字数は常に"A"の1文字だけです。それにも関わらず、追加する元の文字列変数が長ければ長いほど、大きな処理時間を要していることが分かります。


続いて、文字列結合のパフォーマンスを向上させるための方策を検討してみましょう。上記の結果から、ポイントとしては次の3つが考えられます。
  1. 追加される文字列変数の長さをできるだけ短くする

  2. 追加する回数をできるだけ少なくする

  3. 追加するのではなく、元々長い文字列変数に、1文字ずつ当てはめていってはどうか?

まず、1と2は個別に考えるのは難しいので、両方を織り込んだ2種類のコードを考えてみました。


Sub StrTest2_1()

  Dim strData As String
  Dim iintLoop As Integer

  strData = ""
  Debug.Print Timer
  
  For iintLoop = 1 To 20000
    strData = strData & "AAA"
  Next iintLoop
  Debug.Print Timer

End Sub


Sub StrTest2_2()

  Dim strData As String
  Dim strData1 As String
  Dim strData2 As String
  Dim strData3 As String
  Dim iintLoop As Integer

  strData = ""
  Debug.Print Timer
  
  For iintLoop = 1 To 20000
    strData1 = strData1 & "A"
  Next iintLoop
  For iintLoop = 1 To 20000
    strData2 = strData2 & "A"
  Next iintLoop
  For iintLoop = 1 To 20000
    strData3 = strData3 & "A"
  Next iintLoop
  
  strData = strData1 & strData2 & strData3
  Debug.Print Timer

End Sub


StrTest2_1プロシージャでは、1回当たりに追加する文字数を3倍にすることによって、追加処理回数を1/3に抑えています。
一方、StrTest2_2プロシージャでは、ループの回数は前と同じ2万回×3回ですが、1つの文字列に6万字すべてを追加するのではなく、別の3つの変数を用意し、それぞれに2万字ずつ追加し、最後にそれら3つの変数どうしを結合することによって、最終的に6万字の変数を生成しています。

その結果は以下のようになりました。
処理 実行時間
StrTest2_1プロシージャ 12.06 秒
StrTest2_2プロシージャ 2.59秒
最初のテスト結果の27.24秒に比べれば、いずれも大幅に実行時間が改善されています。このことから、「文字列結合においては、その回数および結合される側の変数の長さが短いほど、処理時間を短くできる」ということがいえます。


最後に、3の案をトライしてみましょう。今回は、実際にはレアケースかもしれませんが、あらかじめ6万字が追加されるということが分かっています。そこで、次の2つの方法で、あらかじめ6万字分のメモリ領域を用意しておき、そこにMid$関数を使って1文字ずつ追加する文字をその変数エリアに当てはめていくという処理を行ってみます。
  1. 長さ6万の固定長文字列変数を使う

  2. 可変長文字列変数にあらかじめString$で6万字のNullを代入しておく

Sub StrTest3_1()
'長さ6万の固定長文字列変数を使う

  Dim strData As String * 60000
  Dim ilngLoop As Long

  strData = ""
  Debug.Print Timer
  
  For ilngLoop = 1 To 60000
    Mid$(strData, ilngLoop, 1) = "A"
  Next ilngLoop
  Debug.Print Timer

End Sub


Sub StrTest3_2()
'可変長文字列変数にあらかじめString$で6万字のNullを代入しておく

  Dim strData As String
  Dim ilngLoop As Long

  Debug.Print Timer
  strData = String$(60000, vbNull)
  
  For ilngLoop = 1 To 60000
    Mid$(strData, ilngLoop, 1) = "A"
  Next ilngLoop
  Debug.Print Timer

End Sub


※参考までに、もしあらかじめ6万字というようにその文字数の上限が明確でない場合には、「多少余裕を持った長さでvbNullをセットしておき、最後にInstr関数を使って最初のvbNullの位置を調べ、Left$関数で切り出す」という方法を使うことができます。


この結果は以下のとおりです。
処理 実行時間
StrTest3_1プロシージャ 0.05 秒
StrTest3_2プロシージャ ほとんど0秒
この結果から、"文字列を結合するのではなく当てはめる"という手段は、文字列処理において絶大な効果を上げることが分かったと思います。実は、Mid$関数を使った場合には、その任意の位置に文字を代入する際に、別の領域に変数の内容がいったんコピーされるという内部動作は行われません。その領域に置かれたままで処理が行われるのです。このことからも、いかに文字列変数のコピーという動作がパフォーマンスを下げる要因になるか、そしてできる限りコピー動作を行わせない工夫がパフォーマンスの向上につながるかということが分かったと思います。
| Index | Prev | Next |

 

Copyright © T'sWare All rights reserved