コードの読みやすさと、マクロの処理速度のバランスに関するお話です。
今回のテーマは「Ifの速度=判定回数にどれだけこだわるか」です。
- ひとくちに「IFの処理時間」といっても、3つの要素がある
- =で比較する部分の処理時間は0
- If文で最適化すべきは読みやすさ
- 共通の分岐も同じように分けることができる
- A,Bの処理が遅い場合はどうするか
- おまけ:本質的にはプロシージャに分ける
ひとくちに「IFの処理時間」といっても、3つの要素がある
まずはじめに
If A = B Then 処理C End If
というIf文があったとします。
この処理速度を考えようと思ったときは、
このIfを3つの部分に分けて考えなければいけません。
① | A,B | AとBを求める時間 |
---|---|---|
② | = | 比較する時間 |
③ | 処理C | Ifで分岐したブロックの処理時間 |
まずはこのことを念頭に置いて、読み進めてください。
=で比較する部分の処理時間は0
今回のテーマで、まず意識すべき点は、
②の「=で比較する時間」は、0だと思っていいということです。
単純な判定をするだけなら、
For i = 1 To 100000000 If 1 = 2 Then MsgBox ("常にFalseだから実行されない") Next
↑これは1秒かかりません。
0だと思っていいというか、もう0ですね。
「If文をたくさん使うと遅くなるかな?」
っていうのは、考えなくてもいいのです。
If文で最適化すべきは読みやすさ
ということで、②の比較時間が0でしたから、
「If文の実行時間」=「①A,Bを求める時間」
であることが分かりました。
ここでひとまず、この「A,Bを求める時間」は置いておきましょう。
If Cells(R, 2) = 1 Then
のような0ではないけどだいたい0くらいの処理だと仮定しておいてください。
さてこの前提で、以下のコードを比べてください。
どちらも結果は同じになるコードです。
' ① If Cells(R, 2) = "みかん" Then If Cells(R, 3) = "愛媛" Then 処理X:10行くらい ElseIf Cells(R, 3) = "和歌山" Then 処理Y:10行くらい End If End If
' ② If Cells(R, 2) = "みかん" And Cells(R, 3) = "愛媛" Then 処理X:10行くらい End If If Cells(R, 2) = "みかん" And Cells(R, 3) = "和歌山" Then 処理Y:10行くらい End If
①と②で、If文の書き方を変えてあります。
数学的には①の方が美しいですね。
Ifの判定回数でも、①の方が圧倒的に優れています。
VBAのAnd演算子は、Falseを見つけても最後まで判定しますので、②は
- そもそもみかんかどうか2回判定している
- みかんじゃないときでも、愛媛・和歌山産か判定している
- 愛媛産とわかったあとでも、和歌山産か判定している
と、無駄な判定をしまくっています。
がしかし。
もう何を言いたいかわかると思いますが、
プログラムとしては②の方が優れています。
なぜなら、
「管理しなければいけないElse、EndIfの数が少ない」
⇩
「If ○○ の部分まで遡って見に行く必要が減る」
⇩
「一度に見なければいけない範囲が狭くて済む」
からです。
「ElseとEndIfの場所を間違っちゃう><」とか、
「EndIfの数が足りない><」とか、
そういう初歩的な話ではありません。
(そういうミスも往々にしてやるけど)
①のプログラムは、和歌山みかんの処理Yを書いている最中に、
「みかんのことを書いている」ことが視界に入らない可能性があるのです。
↓こんなイメージ
処理X6 処理X7 処理X8 処理X9 処理X10 処理X11 ElseIf Cells(R, 3) = "和歌山" Then 処理Y1 処理Y2 処理Y3 今ここを書いている。私はみかん? End If End If
和歌山だからまだみかんだとわかりますが、山梨とかだと危ないです。
なーんて油断していると、まさかの和歌山産「紀州南高梅」が登場し、
梅干をみかん箱に詰めてしまう不具合を書いてしまうかもしれません。
まあさすがに今回の例は、Ifの分岐内容がとても簡単なので、
本当にこの程度のIf文なら、①の方がきれいで良いと思います。
(というか、ここまで単純ならSelect Case文にした方が見やすいです)
しかし実際のプログラムでは、こんな単純に書けないIf文はいっぱいあります。
そして、バグ修正や仕様変更などが発生すると、
真っ先に手が入るのがIFの分岐です。
それを「IFが重複してはいけない縛り」プレイで、ElseIf、And、Orを見事に組んでいこうとすると、
どこがどの順番で処理されていくかわからない、いわゆるスパゲティコードが誕生します。
皆さんも一度くらい経験したことがあるのでは?
美しすぎて手が出せないElse地獄を、
キーボードよりも長い時間、マウスのホイールを回しまくって改修する悪夢を。
別にIfは重複しても問題ないのです。
②の方が(無視できる範囲で)遅いプログラムと見せかけて、
思考時間もコストに加えれば、②の方が圧倒的に高速なプログラムだったりしますよ。
共通の分岐も同じように分けることができる
さて、先ほど比較したコードを再掲します。
' ① If Cells(R, 2) = "みかん" Then If Cells(R, 3) = "愛媛" Then 処理X ElseIf Cells(R, 3) = "和歌山" Then 処理Y End If End If
' ② If Cells(R, 2) = "みかん" And Cells(R, 3) = "愛媛" Then 処理X End If If Cells(R, 2) = "みかん" And Cells(R, 3) = "和歌山" Then 処理Y End If
ここで、「みかん共通の処理があったら、②では書けなくない?」という疑問が浮かびますね。
つまりこういうことです。
' ①に共通処理を追加 If Cells(R, 2) = "みかん" Then 共通の処理Z If Cells(R, 3) = "愛媛" Then 処理X ElseIf Cells(R, 3) = "和歌山" Then 処理Y End If End If
たしかに②にこの共通処理は書きづらい気がしますが、
実はそんなことはありません。
↓こう書くことで②でも対応ができます。
If Cells(R, 2) = "みかん" Then 処理Z End If If Cells(R, 2) = "みかん" And Cells(R, 3) = "愛媛" Then 処理X End If If Cells(R, 2) = "みかん" And Cells(R, 3) = "和歌山" Then 処理Y End If
これも慣れないと気持ち悪い書き方かもしれませんが、メリットはあります。
共通の処理がどこにあるか一目瞭然です。
しかし、これは前ほど②が優秀とは言い切れません。
なぜなら、「順序関係が希薄になる」からです。
この辺は場合によりけりで、
- 処理Z⇒処理X,Y の順番で実行する必要がある場合は、それが明確になっている①の方がいい気がします。
- 逆にどの順番でやってもいい場合は、②の方がいい気がします。
「読みやすいコード」を突き詰めれば、当然正解なんてありません。
自分が読みやすいと思う書き方をすればOKです。
大事なのは、「考慮に値しない早さに支配されて、読みやすさの大事さを忘れてしまう」ことが無いように意識することです。
A,Bの処理が遅い場合はどうするか
さて、ひとまず置いておいた「A=Bの実行速度」問題ですが、こちらは単純です。
必要な判定である以上、どうせ1回は実行しなければいけません。
なので、最初に変数に入れてしまいましょう。
そうすれば、Ifの速度とは無関係の話になります。
②のコードを書き換えるとこんな感じ
Dim KEY商品名 As String: KEY商品名 = Cells(R, 2) Dim KEY産地 As String: KEY産地 = Cells(R, 3) If KEY商品名 = "みかん" And KEY産地 = "愛媛" Then 処理A End If If KEY商品名 = "みかん" And KEY産地 = "和歌山" Then 処理B End If
これでA,Bが時間がかかる処理かどうか問題は解決しました。
A,Bの処理が重かろうが、1回で済むので、Ifの書き方は関係なくなります。
余談ですが、ただ「商品名」という変数名にせず、
「分岐条件の商品名」のようにすると、変数の意図が明確になって読みやすく、書きやすくなります。
「分岐条件の」といちいち打つのはだるいので、↑の通り私は「KEY」を接頭しています。
この変数は当然かなり頻繁に出てくるわけですが、
これで日本語入力OFFのまま打てるので便利です。
おまけ:本質的にはプロシージャに分ける
この手の話は大体このおまけで締めくくってますね。
最終的には、プロシージャに分割するのが理想です。
If Cells(R, 2) = "みかん" Then If Cells(R, 3) = "愛媛" Then Call 愛媛みかんに処理Xを行う ElseIf Cells(R, 3) = "和歌山" Then Call 和歌山みかんに処理Yを行う End If End If
こう書くことができれば、
処理XとYがどれほど複雑だろうとメインコード上は1行なので、
①で書いても十分わかります。
むしろ分岐の並列関係が明確なので、
(Elseのおかげで、処理XとYが同時に処理されないことがすぐわかる)
ここまでくれば②より①が読みやすいですね。
また、愛媛と和歌山で処理が大きく変わらないようなときは、
If Cells(R, 2) = "みかん" Then Call みかんの共通処理 Call みかんの産地ごとの処理(Cells(R, 3)) End If
と、産地で分岐する部分をIf文ごと関数にすると、
より分かりやすいコードになるかもしれません。
共通処理は引数がなく、
産地ごとの処理には、産地を引数に渡しているのがわかりやすいですね。
関数は「何回も出てくるコードをまとめるもの」という説明が一般的なようですが、
今回の「どんなに複雑な処理でも、メインコードでは1行のままに保つ」の方が、
便利だし、よく使う使い方だと思います。
こちらの使い方の方が、書くのも簡単です。