和風スパゲティのレシピ

日本語でコーディングするExcelVBA

Sub/Functionプロシージャを呼ぶのにかかる時間

マクロを複数のSubに分割したり、自作関数をたくさん作り始めると、
タイトルの問題が心配になってきます。


処理をSub/Functionに分割することで処理が遅くなることはあるんでしょうか?

これを検証してみましょう。


お忙しい方のために先に結論から述べますと、

1,000万回Callして1~2秒程度しかかからないため、
プロシージャを呼ぶのにかかる時間は0だと思ってOK

が結論です。


検証の詳細が見たい方は読み進めてみてください。

単純なCallにかかる時間

検証コード

まずは「空っぽのSub/FunctionをCall」してみましょう。
単純にCallするだけの時間を計測します。


Timer関数を使い1,000万回分の処理を計測し、
それを10回行って平均値を計算します。

※ 「検証コード」部分だけ読めばOKです(前後はTimer用の処理です)

Sub SubFunctionを単純にCallする時間()

    Dim Arr処理時間記録(1 To 10) As Double
    Dim 開始時刻 As Double, 終了時刻 As Double

    Dim 検証回数 As Long
    For 検証回数 = 1 To 10
        開始時刻 = Timer

        Dim i As Long
        For i = 1 To 10000000

            ' ◇ 検証コード(どちらかをコメントにして実行)
            Call SubをただCallする
            
            Call FunctionをただCallする

        Next

        終了時刻 = Timer
        Arr処理時間記録(検証回数) = 終了時刻 - 開始時刻
    Next

    Debug.Print WorksheetFunction.Average(Arr処理時間記録)

End Sub

Sub SubをただCallする()
End Sub

Sub FunctionをただCallする()
End Sub

結果

SubをただCall 0.53秒
FunctionをただCall 0.77秒

※ Windows10、Intel Core i5-10400、メモリ8G、Excel365

このような結果となりました。
1,000万回でこれなので相当早いですね。

とりあえず「ただCallするのにかかる時間はほぼ0」と思ってよさそうです。


ちなみにFunctionの方がちょっと遅いのは、
返り値のメモリを確保する必要があるからですね。
 

Function FunctionをただCallする()
End Function

これは返り値なしという意味ではなく、

Function FunctionをただCallする() As Variant 
End Function

この省略系という扱いになります。


Variant変数を1,000万個用意する時間が追加されたと思ってください。

そう考えると、変数宣言の時間も無視して良さそうですね。

引数の受け渡しにかかる時間

ただCallするのはそんなに時間を要さないことがわかりました。

続いて引数を渡す場合の処理時間を計算してみましょう。


変数の型の中でもよく使ってかつ処理が遅くなりやすい、
「文字列と文字列の結合」で試します。


「あ」と「い」を「あい」にする処理を、
プロシージャの内外でやってみて比較します。

検証コード

Timer用の処理は先ほどと同じですので、
「検証コード」「使用するプロシージャ」だけ見ればOKです。

Sub 引数の受け渡しにかかる時間()

    Dim Arr処理時間記録(1 To 10) As Double
    Dim 開始時刻 As Double, 終了時刻 As Double

    Dim 結果の格納先 As String
    Dim 引数1 As String: 引数1 = "あ"
    Dim 引数2 As String: 引数2 = "い"

    Dim 検証回数 As Long
    For 検証回数 = 1 To 10
        開始時刻 = Timer

        Dim i As Long
        For i = 1 To 10000000
            結果の格納先 = ""
            
            ' ◇ 検証コード(対象以外をコメントにして実行)

            ' ① プロシージャを分割しない
            結果の格納先 = 引数1 & 引数2

            ' ② ByRefで渡す
            Call 引数をByRefで渡す(結果の格納先, 引数1, 引数2)

            ' ③ ByValで渡す
            Call 引数をbyValで渡す(結果の格納先, 引数1, 引数2)

            ' ④ ByRefにベタ打ちの値を渡す
            Call 引数をByRefで渡す(結果の格納先, "あ", "い")

            ' ⑤ String⇒Variantへ受け渡す
            Call StringをVariantに渡す(結果の格納先, 引数1, 引数2)

            ' ⑥ Functionで結果を受け取る
            結果の格納先 = 結果を返り値で受け取る(引数1, 引数2)

        Next

        終了時刻 = Timer
        Arr処理時間記録(検証回数) = 終了時刻 - 開始時刻
    Next

    Debug.Print WorksheetFunction.Average(Arr処理時間記録)

End Sub

' 使用するプロシージャ
Sub 引数をByRefで渡す(ByRef 結果 As String, ByRef 引数1 As String, ByRef 引数2 As String)
    結果 = 引数1 & 引数2
End Sub
Sub 引数をbyValで渡す(ByRef 結果 As String, ByVal 引数1 As String, ByVal 引数2 As String)
    結果 = 引数1 & 引数2
End Sub
Sub StringをVariantに渡す(ByRef 結果 As String, ByVal 引数1 As Variant, ByVal 引数2 As Variant)
    結果 = 引数1 & 引数2
End Sub
Function 結果を返り値で受け取る(ByRef 引数1 As String, ByRef 引数2 As String) As String
    結果を返り値で受け取る = 引数1 & 引数2
End Function

結果

分割法 処理時間 ①比較
① プロシージャを分割しない 0.94秒 -
② ByRefで渡す 1.48秒 +0.54秒
③ ByValで渡す 2.65秒 +1.71秒
④ ByRefにベタ打ちの値を渡す 2.37秒 +1.44秒
⑤ String⇒Variantへ受け渡す 3.03秒 +2.09秒
⑥ Functionで結果を受け取る 1.61秒 +0.67秒

※ Windows10、Intel Core i5-10400、メモリ8G、Excel365

このような結果となりました。

①が標準の処理時間となりますので、
各処理時間から①を引くと、Call+引数受け渡しによるタイムロスがわかります。


まず特筆すべきは②と⑥でしょう。

0.54秒、0.67秒というのは、前項でCallしただけのコードとほぼ同じ結果です。

つまりByRefで変数を渡すのに時間は要さないということになります。


そして最も遅い⑤ですら、1,000万回で2秒しか増えません。

100万行の満タンExcelファイル全行をループし、
すべての行で10回ずつ関数を使ったとしてやっと+2秒ということです。


これはもうほぼ0といっていい時間じゃないでしょうか?

つまりプロシージャ分割によるタイムロスはほとんどないということですね。


プロシージャ分割の速度について、こんな言及をされることがあります。

  • ByValは変数が増えるからByRefより遅くなる
  • VariantにStringを渡すと型をキャストするから遅くなる
  • 直打ちの値(リテラル)を渡すと遅くなる

実際の実行結果を見てみると、これらは机上ではすべて正しく、
そして実務ではすべて無視してよかったことがわかります。


ということで、結論としては

1,000万回Callして1~2秒程度しかかからないため、
プロシージャを呼ぶのにかかる時間は0だと思ってOK

ということでした。


マクロを複数のプロシージャに分割してコードを読みやすくしたり、
汎用関数を作ってコーディングを早く正確にしたり、
Sub/Functionプロシージャには素晴らしい恩恵があります。


その対価であるタイムロスはほぼ0ですので、
安心してプロシージャを使い倒してください。

おまけ:引数に渡すと重そうな変数

先ほど

変数の型の中でもよく使ってかつ処理が遅くなりやすい、
文字列と文字列の結合で試します。

こう書いていました。

文字列は「長さがどれくらいかわからない」ため、
連結などの計算時に都度メモリを再確保する分時間がかかる傾向があります。


しかし感覚としてはもっと重い変数がある気がしてしまう方もいるかもしれません。


シートやセル、配列など、膨大なデータを保持できる変数ですね。
これらを引数として渡してもCallにかかる時間が増えないのかも解説します。

オブジェクト変数(セルやシートなど)を渡しても遅くならないのか

例えばRangeやWorksheetなどは大量の情報を保持していますが、
これらをSetした変数はどのくらいの情報を持っているのでしょうか。


実はこれらの変数はオブジェクトそのものを変数内に格納しているのではありません。

オブジェクト変数が格納するのはそのオブジェクトが保管されたメモリのアドレスで、
ほとんど数値みたいなものです。


実際のセルやシートという実物がありますからね。
わざわざ変数の中にデータとして持つ必要はなく、住所があればよいのです。


もちろん、変数が持っているのは住所だけのため、

Range("A1").Value

このコードでは「住所を見て値を取りに行く」処理が走ります。

よって処理時間は文字列や数値を扱うよりも当然かかります。


そういう意味では「重い」というのは正しいのですが、
これは「処理が重い」という意味であり、変数の容量は重いわけではありません。


このためオブジェクト変数を引数に渡しても、
Sub/FunctionをCallする時間はLong変数と同程度しか増えません。

安心してWorksheetやRangeを渡しまくってください。


※ これはByVal・ByRefどちらでも同じです。
オブジェクト変数をByValにしたからと言ってオブジェクトが複製されるわけではありません。
オブジェクト変数のByVal/ByRefは「関数内で再Setした際それが元関数に反映されるかどうか」を設定するためのものです。

配列を渡しても遅くならないのか

配列(Array)の場合はオブジェクト変数と違い、
シートやセルなどの実物がどこかにあるわけではありません。


よって、配列をByVal引数に渡した場合は、
配列が複製されてしまいその分時間を要します。

※ 通常配列はByRefでしか渡せないのですが、
ByValのVariant変数に渡すことで、配列をByValで渡すことができます。


まあこれを「Callにかかる時間」と評するのはなんか変な気がしますので、

  • × 配列を使う場合はCallに時間がかかる
  • ○ 配列を再生成(複製)する処理を書けばその分時間がかかる

と解釈すべき部分かと思います。


本当に配列を複製して扱いたい場合を除き、
配列をByValで渡さないよう注意してください。