エクセルVBAの高速化については、さまざまな記事がネット上に公開されています。
エクセルVBAについては、条件にもよりますが、「Do While」も「For Next」も「For Each」も大差はなく、より高速化を図りたければ「配列」を使うのが良いようです。
しかし、ワードVBAについては、「Do While」や「For Next」よりも「For Each」の方がずっと速く、場合によっては「配列」よりも高速です。
このため、VBAアセットの「VA公用文」では、「For Each」を多用して、処理の高速化を図っています。
具体的な速度の差異については、次の2つのテストの結果をご確認ください。
テスト1:文字の挿入
まずは、はじめに紹介した「速度比較決定版」(エクセルを対象にしたもの)にできるだけ近いコードで比較します。
ただし、ワードVBAでは段落のTextに改行記号まで含まれてしまうことや、「For Each」と共通の処理を行う必要があることなどから、あらかじめ「X」という文字が入力されている500個の段落に、1から500までの数字を入力しています。
Sub test1() 'テスト全体の制御
Const lngMax As Long = 500
Application.ScreenUpdating = False
Call rstPars1(lngMax)
Call test1_1(lngMax)
Call rstPars1(lngMax)
Call test1_2(lngMax)
Call rstPars1(lngMax)
Call test1_3(lngMax)
Call rstPars1(lngMax)
Call test1_4(lngMax)
Call rstPars1(lngMax)
Call test1_5(lngMax)
Application.ScreenUpdating = True
End Sub
Sub rstPars1(max As Long) '段落の初期化
Dim par As Paragraph
For Each par In Paragraphs
par.Range.Delete
Next
Dim i As Long
For i = 1 To max
Paragraphs(i).Range.Text = "x" + vbCr
Next i
Paragraphs(max + 1).Range.Delete '最後の余分な改行を削除しています。
End Sub
Sub test1_1(max As Long) 'テスト1「Select」
Dim sngTime As Single
sngTime = Timer
Dim i As Long
For i = 1 To max
Paragraphs(i).Range.Select
Selection.Range.Characters.First = i
Next i
Debug.Print "test1_1(Select):" & Timer - sngTime
End Sub
Sub test1_2(max As Long) 'テスト2「For Next」
Dim sngTime As Single
sngTime = Timer
Dim i As Long
For i = 1 To max
Paragraphs(i).Range.Characters.First = i
Next i
Debug.Print "test1_2(ForNext):" & Timer - sngTime
End Sub
Sub test1_3(max As Long) 'テスト3「Do While」
Dim sngTime As Single
sngTime = Timer
Dim i As Long
i = 1
Do While i <= max
Paragraphs(i).Range.Characters.First = i
i = i + 1
Loop
Debug.Print "test1_3(DoLoop):" & Timer - sngTime
End Sub
Sub test1_4(max As Long) 'テスト4「For Each」
Dim sngTime As Single
sngTime = Timer
Dim i As Long
Dim par As Paragraph
For Each par In Paragraphs
i = i + 1
par.Range.Characters.First = i
Next
Debug.Print "test1_4(ForEach):" & Timer - sngTime
End Sub
Sub test1_5(max As Long) 'テスト5「配列」
Dim sngTime As Single
sngTime = Timer
Dim i As Long
Dim MyAry() As Paragraph
ReDim MyAry(1 To max)
For i = 1 To max
Set MyAry(i) = Paragraphs(i)
Next i
For i = 1 To max
MyAry(i).Range.Characters.First = i
Next i
Debug.Print "test1_5(Array):" & Timer - sngTime
End Sub
配列は、配列上で処理した値を一挙に書き込むようにしたかったのですが、ワードの場合、VBAで複数の段落を選択したり、複数の段落を参照したりすることはできないようです。
このため、オブジェクト配列を用いて、配列上で処理を行うと自動的にドキュメントに反映されるようにしています。
結果は、次のとおりです(単位:秒)。
test1_1(Select): 5.625
test1_2(ForNext): 2.320313
test1_3(DoLoop): 2.382813
test1_4(ForEach): 0.515625
test1_5(Array): 2.21875
「Select」が遅いのはエクセルと同じですが、「For Next」や「Do Loop」に比べて「For Each」が圧倒的に速いのが分かります。
また、「配列」(オブジェクト配列)は、「For Next」や「Do Loop」よりも、やや速い程度にとどまります。
テスト2:インデントの設定
次にVBAアセットが公開している「VA公用文」で主体となるインデントの設定について、同じように比較します。
1から500までの番号が入力された500個の段落に、10ポイントの左インデントを設定しています。
Sub test2() 'テスト全体の制御
Const lngMax As Long = 500
Application.ScreenUpdating = False
Call rstPars2(lngMax)
Call test2_1(lngMax)
Call rstPars2(lngMax)
Call test2_2(lngMax)
Call rstPars2(lngMax)
Call test2_3(lngMax)
Call rstPars2(lngMax)
Call test2_4(lngMax)
Call rstPars2(lngMax)
Call test2_5(lngMax)
Application.ScreenUpdating = True
End Sub
Sub rstPars2(max As Long) '段落の初期化
Dim par As Paragraph
For Each par In Paragraphs
par.Range.Delete
Next
Dim i As Long
For i = 1 To max
Paragraphs(i).Range.Text = i & vbCr
Next i
Paragraphs(max + 1).Range.Delete '最後の余分な改行を削除しています。
End Sub
Sub test2_1(max As Long) 'テスト1「Select」
Dim sngTime As Single
sngTime = Timer
Dim i As Long
For i = 1 To max
Paragraphs(i).Range.Select
Selection.Paragraphs.LeftIndent = 10
Next i
Debug.Print "test2_1(Select):" & Timer - sngTime
End Sub
Sub test2_2(max As Long) 'テスト2「For Next」
Dim sngTime As Single
sngTime = Timer
Dim i As Long
For i = 1 To max
Paragraphs(i).LeftIndent = 10
Next i
Debug.Print "test2_2(ForNext):" & Timer - sngTime
End Sub
Sub test2_3(max As Long) 'テスト3「Do While」
Dim sngTime As Single
sngTime = Timer
Dim i As Long
i = 1
Do While i <= max
Paragraphs(i).LeftIndent = 10
i = i + 1
Loop
Debug.Print "test2_3(DoLoop):" & Timer - sngTime
End Sub
Sub test2_4(max As Long) 'テスト4「For Each」
Dim sngTime As Single
sngTime = Timer
Dim i As Long
Dim par As Paragraph
For Each par In Paragraphs
par.LeftIndent = 10
Next
Debug.Print "test2_4(ForEach):" & Timer - sngTime
End Sub
Sub test2_5(max As Long) ''テスト5「配列」
Dim sngTime As Single
sngTime = Timer
Dim i As Long
Dim MyAry() As Paragraph
ReDim MyAry(1 To max)
For i = 1 To max
Set MyAry(i) = Paragraphs(i)
Next i
For i = 1 To max
MyAry(i).LeftIndent = 10
Next i
Debug.Print "test2_5(Array):" & Timer - sngTime
End Sub
結果は、次のとおりです(単位:秒)。
test2_1(Select): 6.210938
test2_2(ForNext): 2.859375
test2_3(DoLoop): 2.820313
test2_4(ForEach): 0.8828125
test2_5(Array): 2.40625
テスト1と同じく、「For Each」が一番速いのが分かります。
この記事は、ノンプロ研(https://nonproken.shop)の「VBA研究会」での研究成果に基づくものです。
研究にご参加いただけた会員の皆さん、ありがとうございました。
コメント
配列のやり方は、他にもっと良い方法があるかも知れません。
今回行った5つのパターン以外にも、比較テストを行うべきものがあるかも知れません。
なにかアイデアがありましたら、ぜひ、教えてください。
ForEachを使うことではなく段落を変数に入れたことが影響している可能性もあるため、こちらも試してみましたが、テスト5のFor Nextの結果とほどんど変化がありませんでした。
Sub test1_6(max As Long) ‘テスト6「For Next-2」
Dim sngTime As Single
sngTime = Timer
Dim i As Long
Dim pars As Paragraphs
Set pars = ActiveDocument.Paragraphs
For i = 1 To max
pars(i).Range.Characters.First = i
Next
Debug.Print “test1_6(ForNext2):” & Timer – sngTime
End Sub
なぜ、ForEachがForNextより速いのかは分かりません。
ワードはエクセルと違って、マクロで行った操作も一つずつCtrl+Zでアンドゥできるので、そういったところが影響しているのかもしれませんが、ForEachでもForNextと同じようにアンドゥできてしまいます。
不思議です。