今回は、VBAプログラムの処理速度を改善する、ちょいテクについて説明しようと思います。皆さん、システムを利用していて、処理速度に対して、ストレスを感じたことはありませんか?
私は、せっかちなので、こういう場面に遭遇すると、非常にイライラしてストレスを感じてしまいます。
コンピュータ上で動作するシステムのパフォーマンス問題における、一つの原因は、メモリ不足、CPUの処理速度、ディスクのアクセス速度といった、物理的な環境によることが挙げられます。これについては、メモリの増設や、新型のパソコンに交換するといった方法で、概ね改善できるでしょう。
しかし、原因が、開発者の設計したプログラム構造や実装が、よくない場合も可能性として、十分にありえるのです。今回は、プログラムの高速化について、説明しようと思うのですが、『高速なアルゴリズムを採用する』とか、そういった難しい話ではなく、ちょっとしたプログラムの記述の仕方で、改善でできる高速化について、焦点を絞ってお話ししてみたいと思います。
それでは、例を使って、プログラムのパフォーマンス改善について、説明していきます。
以下がパフォーマンスを優先した場合に、良くない一つの例なのですが、皆さん、このプログラムのどこが悪いか分かりますでしょうか?プログラムは、引数に渡されたflgの値によって、ループの中で、処理Aと処理Bに、処理分岐しています。
ちなみに、システム屋さんは、プログラムが処理される、プログラム文中の通り路(経路)のことをパスと呼びます。
'*** パフォーマンス低 ***
Public Sub LowPerform(ByVal flg As Boolean)
Dim i As Long
'1万回のループ
For i = 1 To 10000
If flg Then
'*** flgがTrueのパス ***
'* 処理A *
Debug.Print "処理A"
Else
'*** flgがFalseのパス ***
'* 処理B *
Debug.Print "処理B"
End If
Next i
End Sub
答えは、For文のループ内で、flgの値をチェックしている箇所です。プログラム中に、チェックの箇所が存在しているということは、チェック処理に少なからず時間が、かかっているということです。これって1万回のループなので、1万回のチェック分、処理に時間がかかってしまいます。
ですので、処理速度を優先する場合は、以下のように実装してください。プログラムは変更されていますが、上記のプログラムと、全く同等の処理結果を、もたらすプログラムとなっています。
'*** パフォーマンス高 ***
Public Sub HighPerform(ByVal flg As Boolean)
Dim i As Long
If flg Then
'*** flgがTrueのパス ***
'1万回のループ
For i = 1 To 10000
'* 処理A *
Next i
Else
'*** flgがFalseのパス ***
'1万回のループ
For i = 1 To 10000
'* 処理B *
Next i
End If
End Sub
1万回の条件式の判定処理が、たったの1回に改善できました。このような場合、While文やFor文のループ回数が、増えれば増えるほど、パフォーマンスに、どんどん差がでてきます。『塵も積もれば…』というやつです。
While文やFor文などの繰り返し回数の多いループの中では、if文の条件式判定処理を、できるだけ使用しないというのが、高速なプログラムを記述する一つのポイントです。
但し、デメリットもあります。同じようなループのプログラム・コードが2箇所に記述され、非常にダラダラと冗長的なプログラムになってしまい、プログラムの可読性が下がってしまいます。プログラム自体が読みにくくなると、自身のコーディングミスにもつながり、開発効率も下がってしまいます。
先の良くない例の方が、おそらく、プログラム自体は読みやすく、コーディング量も少なくて済むはずです。ですので、パフォーマンスを優先するべきか、プログラムの可読性やメンテナンス性を優先するべきかを、よく考えて実装することが大切です。
似たような処理で、もう一つ例を使って説明してみたいと思います。以下の、LowPerform2関数の実装を見てください。これも1万回のループ処理なのですが、ループの最後(i=10000)の時だけ、少しことなる処理を行っています。
'*** パフォーマンス低 ***
Public Sub LowPerform2()
Dim i As Long
'1万回のループ
For i = 1 To 10000
'* 処理A *
Debug.Print "処理A"
'ループの最後だけ、少し異なる処理。
If i = 10000 Then
'*** i=10000のパス ***
'* 処理B *
Debug.Print "処理B"
End If
Next i
End Sub
こういった場合は、以下のように、ループの回数を1回減らし、最後に処理Aと処理Bを記述します。ループを抜けた時には、i=10000になっていますので、上記と同等の処理になります。違うのは、ループの中で、If文によるチェック処理が、省略されているところです。If分による条件式の判定が10000回から0回になり、この分、処理が高速化されたことになります。
'*** パフォーマンス高 ***
Public Sub HighPerform2()
Dim i As Long
'9999回のループ
For i = 1 To 9999
'* 処理A *
Debug.Print "処理A"
Next i
'*** i=10000のパス ***
'* 処理A *
Debug.Print "処理A"
'* 処理B *
Debug.Print "処理B"
End Sub
次に、条件式判定を論理演算子で連結した場合の、お話しをしてみたいと思います。VBAでは、以下のようにA And Bや、C Or Dなどのように、複数の条件式判定を論理演算子で、連結すると、実は全て、条件式判定がなされてしまいます。ここに、無駄が発生してしまうのです。
'VBAのIf分による条件式判定
'A,B,C,Dは条件式(値を返却する関数も可)とします。
If A And B Then
Debug.Print "処理"
End If
If C Or D Then
Debug.Print "処理"
End If
もう少し、具体的に説明すると、例えば、A And Bですが、条件式判定Aの結果がFalseだったとします。そうすると、Bの判定結果が、True/Falseのいずれになろうとも、If文内の処理が実行されることはありません。であれば、Bの条件式判定は、なされる必要がないのですが、VBAの仕様上、AもBも、条件式判定されてしまいます。また、論理演算子の種類は異なりますが、C Or Dの場合も、同様のことが言えます。
※ちなみに、C言語は、一つでも、条件式判定が不一致となった時点で、次の処理に移行します。
ですので、上記のIf文をパフォーマンスを優先して書き直すと、以下のようになります。こうすれば、処理が実行されない条件式が存在することになり、少なからず、パフォーマンスが向上します。
'A And Bは、以下のように記述します。
'Bが判定されずに済むケースが発生します。
If A Then
If B Then
Debug.Print "処理"
End If
End If
'C Or Dは、以下のように記述します。
'Dが判定されずに済むケースが発生します。
If C Then
Debug.Print "処理"
ElseIf D Then
Debug.Print "処理"
End If
VBAでは、If文一つに、複数の条件式判定を記述しないほうが、総合的にパフォーマンスが向上します。
このことをもう少し、具体的な例を使って説明したいと思います。以下が、And演算の場合です。処理内容は、100回のループで、iが60~90の場合に、処理を実行するサンプルです。条件式Aを i > 60、条件式Bを i < 90 として、説明を続けます。
'条件式A:i > 60
'条件式B:i < 90
'*** パフォーマンス低 ***
Public Sub LowPerform_And()
Dim i As Long
'100回のループ
For i = 1 To 100
'どちらの条件式も100回判定されます。
If i > 60 And i < 90 Then
Debug.Print "処理"
End If
Next i
End Sub
'*** パフォーマンス高 ***
Public Sub HighPerform_And()
Dim i As Long
'100回のループ
For i = 1 To 100
'100回判定されます。
If i > 60 Then
'iが60より大きい時、40回判定されます。
If i < 90 Then
Debug.Print "処理"
End If
End If
Next i
End Sub
LowPerform_And関数の場合、条件式A、B、いずれも100回判定されてしまいます。HighPerform_And関数のように処理を書き換えた場合、条件式Aは100回、条件式Bは40回判定されるようになり、条件式Bの判定が60回分、処理時間が改善されたことになります。
LowPerform_And関数とHighPerform_And関数は、まったく同等の処理結果をもたらす関数ですが、プログラムの書き方、一つで処理速度に差が出てきます。
今度は、Or演算について、例を使って、考えて見ましょう。以下の、処理内容は、100回のループで、iが60より小さい時、もしくは、90より大きい時に、処理が実行されるサンプルです。条件式Aを i < 60、条件式Bを i > 90 として、説明を続けます。
'条件式A:i < 60
'条件式B:i > 90
'*** パフォーマンス低 ***
Public Sub LowPerform_Or()
Dim i As Long
'100回のループ
For i = 1 To 100
'どちらの条件式も100回判定されます。
If i < 60 Or i > 90 Then
Debug.Print "処理"
End If
Next i
End Sub
'*** パフォーマンス高 ***
Public Sub SpeedHigh_Or()
Dim i As Long
'100回のループ
For i = 1 To 100
'100回判定されます。
If i < 60 Then
Debug.Print "処理"
'iが60以上の時、41回判定されます。
ElseIf i > 90 Then
Debug.Print "処理"
End If
Next i
End Sub
LowPerform_Or関数の場合、条件式A、B、いずれも100回判定されてしまいます。HighPerform_Or関数のように処理を書き換えた場合、条件式Aは100回、条件式Bは41回判定されるようになり、条件式Bの判定が59回分、処理時間が改善されたことになります。
Or演算についても、And演算と同様に、プログラムの書き方、一つで処理速度に差が出てきます。
今回のお話しは、ここまでにしたいと思います。何気に、記述しているプログラムも、ちょこっと変更するだけで、プログラムの処理速度に差がでるものなのです。これが、大容量のデータを扱うループの中だったりすると、とんでもない差が出てくるわけです。そうなんです、『ちょっと』が、『ごっつ』なんです。
今回は、プログラムの書き方、一つで、処理速度に差がでるというお話しをしましたが、メリットだけでは、なく冗長的なプログラムになって、解読しにくいプログラムになるというデメリットも説明しました。
処理速度が求められている場面なのかどうかを、良く考えて、どのような実装にするかを検討して頂けたらと思います。分かりやすいプログラムを書くというのも非常に重要です。
以前にもお話ししましたが、一番大切なのは、正しくプログラムが動作することです。
ご意見・ご要望等ありましたら、画面最下部のメールアドレスまでご連絡ください。