Excel VBA極限加速論:`QueryPerformanceCounter`によるマイクロ秒プロファイリングとCOM境界の超越
「`Application.ScreenUpdating = False` を挟めばVBAは速くなる」——もしあなたが今もその程度の認識で基幹VBAシステムのチューニングに挑んでいるとしたら、厳しい現実を突きつけなければならない。それは最適化の「一歩目」であって、ゴールではない。
10万行を超える大規模なデータ処理、複雑な依存関係を持つブック群の集約、あるいは他システムとのCOM連携において、素人同然のチューニングは気休めにすらならない。本稿では、一般的な解説書が触れようとしないVBAの内部構造(COMアーキテクチャ)に踏み込み、コードが遅い「真の理由」を解剖する。
そして、マイクロ秒単位でボトルネックを穿つプロファイリング技術から、メモリレベルでのデータ転送最適化、さらには例外発生時すら破綻させないエンタープライズ品質の実装パターンまで、すべてを開示する。
—
1. 精度不足の`Timer`関数を捨て、Windows APIへ切り替えよ
ボトルネックを特定する際、VBA標準の `Timer` 関数を使ってはならない。`Timer` 関数の分解能は約15.6ミリ秒(1/64秒)であり、ループ内部の微小な処理遅延や、COMオブジェクトの呼び出しオーバーヘッドを正確に測定することは不可能である。
プロフェッショナルが使用すべきは、Windows APIの `QueryPerformanceCounter` (QPC) および `QueryPerformanceFrequency` である。これにより、CPUクロックに同期したマイクロ秒(1/1,000,000秒)以下の精度でプロファイリングが可能となる。
マイクロ秒単位の精度を持つハイレゾ・タイマー実装
以下は、64ビット(x64)および32ビット(x86)両環境に対応したプロファイルクラス(`clsHighResTimer`)の実装方針である。
Option Explicit
‘ ==============================================================================
‘ クラス名: clsHighResTimer
‘ 概要 : Windows APIを利用した超高精度(マイクロ秒単位)プロファイラ
‘ 補足 : Office 64bit / 32bit 双方に完全対応 (VBA7)
‘ ==============================================================================
If VBA7 Then
Private Declare PtrSafe Function QueryPerformanceCounter Lib “kernel32” (ByRef lpPerformanceCount As Currency) As Long
Private Declare PtrSafe Function QueryPerformanceFrequency Lib “kernel32” (ByRef lpFrequency As Currency) As Long
Else
Private Declare Function QueryPerformanceCounter Lib “kernel32” (ByRef lpPerformanceCount As Currency) As Long
Private Declare Function QueryPerformanceFrequency Lib “kernel32” (ByRef lpFrequency As Currency) As Long
End If
Private m_Frequency As Double
Private m_StartCount As Currency
Private m_StopCount As Currency
Private Sub Class_Initialize()
Dim freq As Currency
‘ システムのCPU周波数を取得 (Currency型を利用して64bit整数を受け取る)
If QueryPerformanceFrequency(freq) <> 0 Then
m_Frequency = CDbl(freq)
Else
Err.Raise vbObjectError + 513, “clsHighResTimer”, “High-resolution counter not supported.”
End If
End Sub
‘ 測定開始
Public Sub StartTimer()
QueryPerformanceCounter m_StartCount
End Sub
‘ 測定停止
Public Sub StopTimer()
QueryPerformanceCounter m_StopCount
End Sub
‘ 経過時間を「ミリ秒」単位で取得 (小数点以下まで高精度に返す)
Public Property Get ElapsedMilliseconds() As Double
If m_Frequency = 0 Then Exit Property
ElapsedMilliseconds = ((CDbl(m_StopCount) – CDbl(m_StartCount)) / m_Frequency) 1000#
End Property
‘ 経過時間を「マイクロ秒」単位で取得
Public Property Get ElapsedMicroseconds() As Double
If m_Frequency = 0 Then Exit Property
ElapsedMicroseconds = ((CDbl(m_StopCount) – CDbl(m_StartCount)) / m_Frequency) 1000000#
End Property
`Currency` 型を利用している点に注目してほしい。`QueryPerformanceCounter` が返す64ビット整数を、VBAの整数オーバーフローを起こさずに高精度かつ高速にハンドリングするための極限のテクニックである。
—
2. なぜそのコードは遅いのか?:COMマーシャリングの壁
プロファイラーで測定すると、セルに対するループ処理(例: `For i = 1 To 100000 : Cells(i, 1).Value = … : Next i`)が壊滅的に遅いことが浮き彫りになる。
この遅延の根本原因はCOM(Component Object Model)の境界を越えるオーバーヘッド(マーシャリング)にある。
[ VBA実行エンジン (VBE) ]
│
│ COM IDispatch / VTable 経由の呼び出し (高コスト)
▼
[ Excel C++ 核心エンジン (Worksheet/Range Object) ]
VBAから `Cells(i, 1).Value` を1回呼び出すたびに、メモリ空間のプロセス境界を越え、引数の評価、プロパティのセキュリティチェック、内部変更通知イベントの発火、画面描画キューへの積み込みが発生する。10万回のループは、10万回のCOM呼び出しを意味する。これが遅くならないわけがない。
対策:Variant配列への一括転送(バルク処理)
最適化の鉄則は、「Excelオブジェクトへのアクセス回数を最小(可能なら1回)にすること」だ。
セル範囲(Range)を一度Variant配列に吸い上げ、VBAの純粋なメモリ空間(スタック/ヒープ)内で処理を完了させた後、一気にセルへ書き戻す。これにより、COM呼び出しは2回(読み込み1回、書き込み1回)に削減される。
Public Sub Optimized_Array_Processing()
Dim timer As New clsHighResTimer
Dim targetRange As Range
Dim dataBuffer As Variant
Dim r As Long, c As Long
Dim rowCount As Long, colCount As Long
Set targetRange = ThisWorkbook.Sheets(1).Range(“A1:E100000”)
rowCount = targetRange.Rows.Count
colCount = targetRange.Columns.Count
timer.StartTimer
‘ 【COMアクセス 1回目】 RangeからVBA内部メモリ(Variant配列)へ一括転送
dataBuffer = targetRange.Value
‘ VBA純粋メモリ空間での高速ループ処理 (COM呼び出しは一切発生しない)
For r = 1 To rowCount
For c = 1 To colCount
If IsNumeric(dataBuffer(r, c)) Then
dataBuffer(r, c) = dataBuffer(r, c) 1.1 ‘ 例: 10%の単価アップ処理
End If
Next c
Next r
‘ 【COMアクセス 2回目】 処理済み配列をRangeへ一括書き戻し
targetRange.Value = dataBuffer
timer.StopTimer
Debug.Print “10万行×5列 バルク処理完了時間: ” & _
Format$(timer.ElapsedMilliseconds, “0.000”) & ” ms”
End Sub
セル直打ちのループと比較した場合、このパターンは通常50倍〜100倍以上の速度向上をもたらす。
—
3. `ScreenUpdating` 以外の隠れた「パフォーマンス・キラー」
画面更新停止(`ScreenUpdating = False`)は第一歩に過ぎない。VBAの実行スピードを根底から削ぎ落とす「5つの隠れた要因」を攻略せよ。
① 計算エンジンのサスペンド(`Calculation`)
数式が設定されたシートに対してデータを書き込むと、1セル変更されるたびにExcel全域の再計算ツリーが評価される。
- 対策: `Application.Calculation = xlCalculationManual`
② イベントハンドラの連鎖カット(`EnableEvents`)
`Worksheet_Change` や `Workbook_SheetChange` などのイベントフックが有効なままデータを大量書き込みすると、裏で何万回ものイベントプロシージャが起動し、メモリを圧迫する。
- 対策: `Application.EnableEvents = False`
③ アーリーバインディング(事前バインディング)の徹底
他アプリケーション(Officeアプリ、Scripting.FileSystemObject、ADODB等)との連携時、`CreateObject(“Scripting.Dictionary”)` などの遅延バインディング(Late Binding)を使用すると、実行時に毎回 `IDispatch::GetIDsOfNames` によるメソッドの動的解決が発生する。
- 対策: 参照設定を行い、`Dim dict As New Scripting.Dictionary`(Early Binding)として宣言する。これにより、コンパイル時にVTableのオフセットが確定し、呼び出しが最速化される。
④ ドット演算子による暗黙オブジェクト生成の回避
一見何気ないコード `Workbooks(“Data.xlsx”).Worksheets(“Sheet1”).Range(“A1”).Font.Color = vbRed` は、裏で参照カウント管理を伴う中間COMオブジェクトを多数生成している。
- 対策: `With` 構文を活用するか、オブジェクト変数に明示的に代入(`Set`)して使い回す。
⑤ COM参照の明示的解放(オブジェクトのライフサイクル制御)
VBAのガベージコレクションは参照カウント方式(`IUnknown::Release`)である。巨大なループ内で生成した外部オブジェクト(例: `ADODB.Recordset` や `RegExp`)を解放せずに回すと、メモリリークのような状態を起こし、処理速度が徐々に低下(劣化)していく。
- 対策: ループ内で生成したCOMオブジェクトは、使用後に必ず `Set obj = Nothing` で明示的に参照を途切れさせる。
—
4. エンタープライズ品質の最適化アーキテクチャ(RAIIパターンの適用)
最適化のために `Application.Calculation` や `Application.EnableEvents` を変更した際、最も恐れるべきは「処理の途中でエラーが発生し、設定が戻らないこと」である。手動計算のまま取り残されたユーザーのワークシートは、業務上の重大な計算ミスを引き起こす。
このリスクを完全に排除するため、C++等で用いられるRAII(Resource Acquisition Is Initialization)思想をVBAのクラスモジュールで再現する。
高速化環境管理クラス:`clsSpeedContext`
以下のクラスを作成し、実行環境の最適化と復元を完全に自動化・カプセル化する。
Option Explicit
‘ ==============================================================================
‘ クラス名: clsSpeedContext
‘ 概要 : VBA高速化設定の適用と、スコープ終了時の確実な自動復元を行うスコープガード
‘ 思想 : RAIIパターン (Class_Terminateによる安全なデストラクタ処理)
‘ ==============================================================================
Private m_OldScreenUpdating As Boolean
Private m_OldDisplayAlerts As Boolean
Private m_OldEnableEvents As Boolean
Private m_OldCalculation As XlCalculation
Private Sub Class_Initialize()
‘ 1. 現在の状態を保存
m_OldScreenUpdating = Application.ScreenUpdating
m_OldDisplayAlerts = Application.DisplayAlerts
m_OldEnableEvents = Application.EnableEvents
m_OldCalculation = Application.Calculation
‘ 2. 極限の高速化設定を適用
Application.ScreenUpdating = False
Application.DisplayAlerts = False
Application.EnableEvents = False
Application.Calculation = xlCalculationManual
End Sub
Private Sub Class_Terminate()
‘ スコープを抜けた際(エラー終了時含む)、必ず元の状態に復元
On Error Resume Next
Application.Calculation = m_OldCalculation
Application.EnableEvents = m_OldEnableEvents
Application.DisplayAlerts = m_OldDisplayAlerts
Application.ScreenUpdating = m_OldScreenUpdating
On Error GoTo 0
End Sub
clsSpeedContext の実戦的な使用例
Public Sub Enterprise_Data_Processing()
On Error GoTo ErrorHandler
‘ スコープガードの生成 (この時点で高速化設定が適用される)
Dim speedGuard As clsSpeedContext
Set speedGuard = New clsSpeedContext
Dim perfTimer As New clsHighResTimer
perfTimer.StartTimer
‘ ————————————————————-
‘ ここにメインの超高速処理ロジックを記述 (Variant配列処理など)
‘ ————————————————————-
Call PerformHeavyTask
perfTimer.StopTimer
Debug.Print “全処理完了時間: ” & Format$(perfTimer.ElapsedMilliseconds, “0.000”) & ” ms”
CleanExit:
‘ 関数を抜ける際に speedGuard が破棄され、
‘ Class_Terminate により自動的に設定が100%元通りに復元される
Set speedGuard = Nothing
Exit Sub
ErrorHandler:
‘ 適切なエラーハンドリングとログ記録
MsgBox “エラーが発生しました: ” & Err.Description, vbCritical, “システムエラー”
Resume CleanExit
End Sub
Private Sub PerformHeavyTask()
‘ ダミーの重い処理
Dim i As Long
For i = 1 To 1000000
‘ 演算処理
Next i
End Sub
このアーキテクチャを採用することで、コードの可読性は飛躍的に向上し、`On Error GoTo` による不格好な復元処理を各プロシージャに散らかす必要は完全になくなる。
—
5. 結論:VBAを掌握するためのマインドシフト
Excel VBAの実行速度に不満を感じたとき、安易に「VBAは遅い言語だ」と結論付けるのは無知の証明に過ぎない。
1. プロファイリング: `QueryPerformanceCounter` でマイクロ秒単位の正確な数値を把握する。
2. アーキテクチャの理解: COM境界の行き来(マーシャリング)を減らし、Variant配列でメモリ内処理に持ち込む。
3. 環境の完全制御: RAIIパターンを用いて、確実かつ安全にExcelの評価エンジンをサスペンドする。
これらの真髄を理解したとき、あなたの書くVBAコードはスクリプトの域を脱し、ネイティブアプリに匹敵する極限のパフォーマンスを発揮する『システム』へと進化を遂げるはずだ。
