#37 If〜ElseステートメントとIIf関数の比較

結論
IIf関数はIf〜Elseステートメントに比べて、単なる固定値の代入であって処理時間がかかる。そしてIIf関数を入れ子にすると倍々で処理時間が増える。その大きな要因は、IIf関数では条件式の真偽に関わらず2つの式両方が実行されているためで、特に組み込み関数やプロシージャを呼び出す場合には十分注意する必要がある。
しかし、ループ内で何度も使う場合には、コードの見た目よりパフォーマンス重視でIf〜Elseステートメントを使った方がよいが、プロシージャの中でたった1回だけ分岐処理を行う程度のものであれば、コードの見易さを重視してIIf関数を使っても特に影響はない。

VBAで条件分岐を行うには、If〜ThenステートメントやIf〜Elseステートメントを使います。一方、IIf関数は、条件によって異なる単一の値を返すような場合にIf〜Elseステートメントの代用として使える関数です。If〜Elseステートメントのように各条件ごとに複数行の命令を記述することはできませんが、コードをシンプルに記述できる便利な関数です。条件による代入値の切り替えを1行で表現できるとともに、そのような1つの変数への代入操作のコードであることが分かりやすくなるというメリットがあります。

しかし、IIf関数も多用する際は注意が必要と言われています。それは、条件式の真偽に応じて設定される2つの値、つまりIIf関数の構文である、


RetValue = IIf(Expr, TruePart, FalsePart)


の2番目と3番目に書かれた処理が常に両方実行されるためです。これらが単なる値であれば大きな問題はないはずですが、組み込み関数やFunctionプロシージャを呼び出しているような場合には、常にそれらが実行されることになってしまいます。特にFunctionプロシージャでテーブル操作などの長い処理、時間のかかる処理を行って1つの返り値を求めているような場合には、その両方が実行されることはパフォーマンスに大きな影響を与えることも考えられます。

そこでここでは、If〜ElseステートメントとIIf関数の実行時間を実際に比較してみるとともに、IIf関数の内部動作、つまり2つの引数が両方実行されるということを検証してみたいと思います。


それではまず手始めに、非常に簡単なループ処理によって2つの処理時間を比べてみたいと思います。次のテスト用コードでは、ループ変数ilngLoopを使って9999999回の繰り返し処理を行っています。そして、ilngLoopの値によって、lngRetという変数に異なる値を代入しています。

Sub iftest_1()
'IfとIIfの簡単な処理の比較(単一の比較演算)

  Dim ilngLoop As Long
  Dim lngRet As Long

  ts_Watch "処理開始", True

  For ilngLoop = 1 To 9999999
    If ilngLoop > 100000 Then
      lngRet = 1
    Else
      lngRet = 2
    End If
  Next ilngLoop

  ts_Watch "If〜Elseステートメント"

  For ilngLoop = 1 To 9999999
    lngRet = IIf(ilngLoop > 100000, 1, 2)
  Next ilngLoop

  ts_Watch "IIf関数"

End Sub



その結果は次のようなものになりました。
If〜Elseステートメント 1.8 秒
IIf関数 4.8 秒
ご覧のように、基本的にはまったく同じ"条件による代入値の切り替え"を行っているだけにも関わらず、2倍以上の開きが出ています。このことから、IIf関数内でFunctionプロシージャを呼び出すまでもなく、単なる固定値の代入であってIIf関数は時間がかかるということが分かります。


さて、続いて、同様のループ処理ですが、今度は条件式を2つにしてみます。IIf関数の場合は引数の中にさらにIIf関数を含めるという入れ子状になります。

そのテスト用コードおよび実行結果は以下のとおりです。

Sub iftest_2()
'IfとIIfの簡単な処理の比較(2つの比較演算)

  Dim ilngLoop As Long
  Dim lngRet As Long

  ts_Watch "処理開始", True

  For ilngLoop = 1 To 9999999
    If ilngLoop > 100000 And ilngLoop < 500000 Then
      lngRet = 1
    Else
      lngRet = 2
    End If
  Next ilngLoop

  ts_Watch "If〜Elseステートメント"

  For ilngLoop = 1 To 9999999
    lngRet = IIf(ilngLoop > 100000, IIf(ilngLoop < 500000, 1, 2), 2)
  Next ilngLoop

  ts_Watch "IIf関数"

End Sub



If〜Elseステートメント 2.1 秒
IIf関数 9.7 秒
IIf関数の方が遅いのは当然のこととして、ここで着目すべきは、最初のテスト結果との違いです。
If〜Elseステートメントの方は17%の増加に過ぎませんが、IIf関数は約100%の増加、つまり2倍にまで処理時間が増大しています。つまり、"IIf関数を入れ子にすると倍々で処理時間が増える"と推測することができます。これが"IIf関数は多用すべきでない"という根拠といえそうです。.....もっとも、入れ子のIIf関数はプログラムの視認性も悪くなるのですが


さてここで、IIf関数の内部メカニズムについて検証を行っておきましょう。

次のようなコードを実行してみます(実際に実行するのはiftest_3)。ここではIIf関数の2番目と3番目の引数で、引数をイミディエイトウィンドウに出力するだけのFunctionプロシージャ「dummy」を呼び出しています。

Sub iftest_3()
'IIf関数の動作の仕組みの検証

  Dim a

  a = 1

  If a = 1 Then
    dummy "Ifその1"
  Else
    dummy "Ifその2"
  End If

  a = IIf(a = 1, dummy("IIfその1"), dummy("IIfその2"))

End Sub


Function dummy(strText As String)

  Debug.Print strText

End Function


実行後のイミディエイトウィンドウは下図のとおりです。
実行後のイミディエイトウィンドウ

当然のことながら、変数「a」が"1"のときは、Elseステートメント内のコードは実行されません。しかしIIf関数はそうはなりません。「a = 1」という条件式の真偽に関わらず2つの式両方が実行されているのは明らかです。

このことからも、パフォーマンス的な理由だけでなく、組み込み関数やプロシージャの"予期せぬ"実行による問題を防ぐためには、IIf関数内での関数やプロシージャ呼び出しには十分注意する必要があるといえるでしょう。

それでは、実際にIIf関数内から組み込み関数を呼び出した場合の処理時間を測定してみることにしましょう。

Sub iftest_4()
'関数を呼び出す場合のIfとIIfの比較

  Dim ilngLoop As Long
  Dim dblRet As Double

  ts_Watch "処理開始", True

  For ilngLoop = 1 To 9999999
    If ilngLoop > 100000 Then
      dblRet = 1 / Sin(ilngLoop)
    Else
      dblRet = 1 / Cos(ilngLoop)
    End If
  Next ilngLoop

  ts_Watch "If〜Elseステートメント"

  For ilngLoop = 1 To 9999999
    dblRet = IIf(ilngLoop > 100000, 1 / Sin(ilngLoop), 1 / Cos(ilngLoop))
  Next ilngLoop

  ts_Watch "IIf関数"

End Sub


ここでは、特に深い意味はありませんが、小数を扱い複雑な処理を行っていそうなSin関数とCos関数を呼び出しています。これまでの結果から、If〜Elseステートメントの方は、Sin関数・Cos関数のいずれか1つだけが呼び出され、IIf関数の方は常に両方が呼び出されることになりますので、後者の方は少なくても2倍の処理時間がかかることが予想されます。

そしてその結果は次のようなものとなりました。
If〜Elseステートメント 3.8 秒 最初のテストの2.1倍
IIf関数 11.3 秒 最初のテストの2.4倍
ご覧のように、単に値を代入するのではなく、関数を実行していることから、どちらも最初のテストに対して実行時間が2倍以上となっています。IIf関数が遅いのはここでも言うまでもありませんが、その分、両者の時間差も大きくなることが分かります。したがって、ただでさえ時間のかかる関数やプロシージャを、数学的な処理などにおいてループで何度も呼び出す場合にはコードの見た目よりパフォーマンス重視でIf〜Elseステートメントを使った方がよいといえます。

とはいえ、違う見方をすれば、プロシージャの中でたった1回だけ分岐処理を行う程度のものであれば、1回当たりについては次表程度の違いしかないので、コードの見易さを重視してIIf関数を使うようにしても何ら影響はないといえるでしょう。
実行結果1回当たりの換算表

テスト1 テスト2 テスト3
If〜Elseステートメント 0.18 0.21 0.38
IIf関数 0.48 0.97 1.13
※各結果を9999999で割ったもので、単位はμ秒
| Index | Prev | Next |

 

Copyright © T'sWare All rights reserved