<p>本記事は<strong>Geminiの出力をプロンプト工学で整理した業務ドラフト(未検証)</strong>です。</p>
<h1 class="wp-block-heading">VBAでWin32 APIを用いた高精度タイマーの実装と最適化</h1>
<h2 class="wp-block-heading">背景と要件</h2>
<p>VBAにおける処理時間の計測には、通常、組み込みの<code>Timer</code>関数が使用されます。しかし、<code>Timer</code>関数は秒単位の精度しか持たず、ミリ秒以下の高精度な時間計測には不向きです。特に、以下のようなシナリオでは、より高精度なタイマーが求められます。</p>
<ul class="wp-block-list">
<li><p><strong>性能測定とボトルネック特定</strong>: 複雑なVBAコードの実行時間を詳細に分析し、性能改善の余地がある部分を特定する。</p></li>
<li><p><strong>リアルタイム処理</strong>: シミュレーションや外部デバイスとの連携など、厳密な時間管理が必要な処理。</p></li>
<li><p><strong>細かなチューニング効果の検証</strong>: コードの最適化前後で、微細な性能向上を数値で確認する。</p></li>
</ul>
<p>この要件を満たすため、本記事ではWindowsの低レベルな機能を提供する<strong>Win32 API</strong>をVBAから利用し、高精度なタイマーを実装する方法について解説します。外部ライブラリに依存せず、標準機能のみで実現することを目標とします。</p>
<h2 class="wp-block-heading">設計</h2>
<h3 class="wp-block-heading">高精度タイマーの選定</h3>
<p>Win32 APIには複数の時間計測関数が存在しますが、精度と用途によって使い分けが必要です。</p>
<ol class="wp-block-list">
<li><p><strong><code>QueryPerformanceCounter</code> / <code>QueryPerformanceFrequency</code> [1, 2]</strong>:</p>
<ul>
<li><p><strong>特徴</strong>: システムの高性能カウンタを使用するため、マイクロ秒〜ナノ秒単位の非常に高い精度を提供します。CPUのクロックサイクルに基づいており、短時間の厳密な計測に適しています。</p></li>
<li><p><strong>用途</strong>: 処理性能のベンチマーク、クリティカルな時間計測。</p></li>
</ul></li>
<li><p><strong><code>timeGetTime</code> [3]</strong>:</p>
<ul>
<li><p><strong>特徴</strong>: マルチメディアタイマーAPIの一部で、デフォルトでは約10〜16ミリ秒の解像度を持ちます。<code>timeBeginPeriod</code> [4]関数と併用することで、システム全体のタイマー解像度を1ミリ秒に向上させることが可能です。</p></li>
<li><p><strong>用途</strong>: ミリ秒単位での計測、一定間隔でのイベント発生。</p></li>
</ul></li>
<li><p><strong><code>GetTickCount64</code> [5]</strong>:</p>
<ul>
<li><p><strong>特徴</strong>: システム起動からのミリ秒数を返す64ビット関数です。精度は<code>timeGetTime</code>と同程度で、約10〜16ミリ秒です。32ビット版の<code>GetTickCount</code>が抱える約49.7日での値のオーバーフロー問題を解決しています。</p></li>
<li><p><strong>用途</strong>: システムの稼働時間計測、比較的長い期間の処理時間計測。</p></li>
</ul></li>
</ol>
<p><strong>比較表:</strong></p>
<figure class="wp-block-table"><table>
<thead>
<tr>
<th style="text-align:left;">タイマー関数</th>
<th style="text-align:left;">精度</th>
<th style="text-align:left;">特徴</th>
<th style="text-align:left;">利点</th>
<th style="text-align:left;">欠点</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:left;"><code>Timer</code> (VBA組み込み)</td>
<td style="text-align:left;">1秒</td>
<td style="text-align:left;">VBA標準。</td>
<td style="text-align:left;">手軽に利用可能。</td>
<td style="text-align:left;">精度が低く、短時間計測には不向き。</td>
</tr>
<tr>
<td style="text-align:left;"><code>GetTickCount64</code></td>
<td style="text-align:left;">約10-16ミリ秒</td>
<td style="text-align:left;">システム起動からのミリ秒。64ビット。</td>
<td style="text-align:left;">オーバーフローの心配が少ない。</td>
<td style="text-align:left;">精度が低く、<code>timeGetTime</code>より汎用性で劣る。</td>
</tr>
<tr>
<td style="text-align:left;"><code>timeGetTime</code></td>
<td style="text-align:left;">約10-16ミリ秒 (デフォルト)<br/>1ミリ秒 (<code>timeBeginPeriod</code>併用時)</td>
<td style="text-align:left;">マルチメディアタイマー。システム全体のタイマー解像度を調整可能。</td>
<td style="text-align:left;">比較的シンプル。ミリ秒精度が必要な場合に有効。</td>
<td style="text-align:left;"><code>timeBeginPeriod</code>と<code>timeEndPeriod</code>のペアリングが必須。システムリソースを消費。</td>
</tr>
<tr>
<td style="text-align:left;"><code>QueryPerformanceCounter</code></td>
<td style="text-align:left;">マイクロ秒〜ナノ秒</td>
<td style="text-align:left;">ハードウェアの高性能カウンタを使用。</td>
<td style="text-align:left;">最高の精度。</td>
<td style="text-align:left;"><code>QueryPerformanceFrequency</code>で正規化が必要。オーバーヘッドがごくわずかに存在。</td>
</tr>
</tbody>
</table></figure>
<p>、最高の精度を提供する<strong><code>QueryPerformanceCounter</code></strong>をメインに、汎用性の高い<strong><code>timeGetTime</code></strong>を比較対象として取り上げます。</p>
<h3 class="wp-block-heading">処理フロー</h3>
<p>タイマーを利用した処理の一般的な流れを以下に示します。</p>
<div class="wp-block-merpress-mermaidjs diagram-source-mermaid"><pre class="mermaid">
graph TD
A["VBAプロシージャ開始"] --> B{"タイマー初期化"};
B --> C{"Win32 API選択"};
C -- QueryPerformanceCounter を使用 --> D["QPC_StartCount 取得"];
C -- timeGetTime を使用 --> E["TimeGetTime_Start 取得"];
D --> F["実行対象のVBAコード"];
E --> F;
F --> G{"タイマー終了"};
G -- QueryPerformanceCounter を使用 --> H["QPC_EndCount 取得"];
G -- timeGetTime を使用 --> I["TimeGetTime_End 取得"];
H --> J["時間差を計算 (QPC_EndCount - QPC_StartCount) / QPC_Frequency"];
I --> K["時間差を計算 (TimeGetTime_End - TimeGetTime_Start) / 1000#"];
J --> L["結果表示またはログ記録"];
K --> L;
L --> M["VBAプロシージャ終了"];
</pre></div>
<h2 class="wp-block-heading">実装</h2>
<p>VBAでWin32 APIを使用するには、<code>Declare PtrSafe</code>ステートメントを使ってAPI関数を宣言する必要があります。ExcelやAccessのVBAエディタで、標準モジュールに以下のコードを記述します。</p>
<h3 class="wp-block-heading">Win32 API宣言</h3>
<pre data-enlighter-language="generic">' このコードは、VBA7 (64ビットOffice) およびVBA6 (32ビットOffice) の両方に対応しています。
' PtrSafe は VBA7 での64ビット互換性確保のために必要です。
' === QueryPerformanceCounter (高精度タイマー) ===
' lpPerformanceCount: 現在のカウンタ値
' lpFrequency: カウンタの周波数
#If VBA7 Then ' Office 2010以降 (64ビット版を含む)
Private Declare PtrSafe Function QueryPerformanceCounter Lib "kernel32" (lpPerformanceCount As Currency) As Long
Private Declare PtrSafe Function QueryPerformanceFrequency Lib "kernel32" (lpFrequency As Currency) As Long
#Else ' Office 2007以前 (32ビット版のみ)
Private Declare Function QueryPerformanceCounter Lib "kernel32" (lpPerformanceCount As Any) As Long
Private Declare Function QueryPerformanceFrequency Lib "kernel32" (lpFrequency As Any) As Long
#End If
' === timeGetTime (マルチメディアタイマー) ===
' timeGetTime: システム時刻をミリ秒で取得
' timeBeginPeriod: システムタイマーの解像度を設定
' timeEndPeriod: システムタイマーの解像度を元に戻す
#If VBA7 Then
Private Declare PtrSafe Function timeGetTime Lib "winmm.dll" () As Long
Private Declare PtrSafe Function timeBeginPeriod Lib "winmm.dll" (ByVal uPeriod As Long) As Long
Private Declare PtrSafe Function timeEndPeriod Lib "winmm.dll" (ByVal uPeriod As Long) As Long
#Else
Private Declare Function timeGetTime Lib "winmm.dll" () As Long
Private Declare Function timeBeginPeriod Lib "winmm.dll" (ByVal uPeriod As Long) As Long
Private Declare Function timeEndPeriod Lib "winmm.dll" (ByVal uPeriod As Long) As Long
#End If
' === グローバル変数 ===
Private s_QPC_StartCount As Currency ' QueryPerformanceCounterの開始カウンタ値
Private s_QPC_Frequency As Currency ' QueryPerformanceCounterの周波数
Private s_timeGetTime_Start As Long ' timeGetTimeの開始ミリ秒
Private s_timeGetTime_PeriodSet As Boolean ' timeBeginPeriodが設定されたか
</pre>
<h3 class="wp-block-heading">タイマーヘルパー関数</h3>
<p>上記のAPIを使って、タイマーを簡単に利用するためのヘルパー関数を作成します。</p>
<pre data-enlighter-language="generic">' QueryPerformanceCounter を使った高精度タイマーの開始
Public Sub StartHighResTimer()
' 周波数を一度だけ取得
If s_QPC_Frequency = 0 Then
If QueryPerformanceFrequency(s_QPC_Frequency) = 0 Then
Err.Raise vbObjectError + 1000, "StartHighResTimer", "QueryPerformanceFrequency API呼び出しに失敗しました。"
End If
End If
' 開始カウンタ値を取得
If QueryPerformanceCounter(s_QPC_StartCount) = 0 Then
Err.Raise vbObjectError + 1001, "StartHighResTimer", "QueryPerformanceCounter API呼び出しに失敗しました。"
End If
End Sub
' QueryPerformanceCounter を使った高精度タイマーの終了 (秒単位で結果を返す)
Public Function StopHighResTimer() As Double
Dim EndCount As Currency
If QueryPerformanceCounter(EndCount) = 0 Then
Err.Raise vbObjectError + 1002, "StopHighResTimer", "QueryPerformanceCounter API呼び出しに失敗しました。"
End If
' (終了カウンタ - 開始カウンタ) / 周波数 = 経過秒数
StopHighResTimer = CDbl((EndCount - s_QPC_StartCount) / s_QPC_Frequency)
End Function
' timeGetTime を使ったミリ秒タイマーの開始 (解像度を1msに設定)
Public Sub StartMillisecondTimer()
' システムタイマーの解像度を1msに設定
' 失敗しても続行可能だが、精度はシステムデフォルトになる
If timeBeginPeriod(1) = 0 Then
s_timeGetTime_PeriodSet = True
Else
Debug.Print "警告: timeBeginPeriod(1) の設定に失敗しました。システムデフォルトの解像度を使用します。"
s_timeGetTime_PeriodSet = False
End If
s_timeGetTime_Start = timeGetTime()
End Sub
' timeGetTime を使ったミリ秒タイマーの終了 (秒単位で結果を返す)
Public Function StopMillisecondTimer() As Double
Dim EndTime As Long
EndTime = timeGetTime()
' timeBeginPeriodで設定した解像度を元に戻す
If s_timeGetTime_PeriodSet Then
timeEndPeriod 1
s_timeGetTime_PeriodSet = False
End If
StopMillisecondTimer = CDbl(EndTime - s_timeGetTime_Start) / 1000# ' ミリ秒を秒に変換
End Function
</pre>
<h3 class="wp-block-heading">コード例1: Excelでのデータ処理と性能チューニング</h3>
<p>Excelで大量のセルにデータを書き込む際の性能を、高精度タイマーで計測し、配列バッファや画面更新制御による最適化の効果を数値で示します。</p>
<p><strong>実行手順:</strong></p>
<ol class="wp-block-list">
<li><p>上記「Win32 API宣言」と「タイマーヘルパー関数」のコードを標準モジュールに貼り付けます。</p></li>
<li><p>以下のコードを別の標準モジュールに貼り付けます。</p></li>
<li><p><code>TestExcelPerformance</code>を実行します。シート1に結果が表示されます。</p></li>
</ol>
<pre data-enlighter-language="generic">' モジュール名: Module1 (または任意の標準モジュール)
Sub TestExcelPerformance()
Const NUM_ROWS As Long = 10000 ' 処理対象の行数
Const NUM_COLS As Long = 10 ' 処理対象の列数
Dim ws As Worksheet
Dim r As Long, c As Long
Dim startTime As Double
Dim dataArray() As Variant
Dim i As Long
Set ws = ThisWorkbook.Sheets(1)
ws.Cells.ClearContents ' シートをクリア
' --- 最適化なし (セルごとの書き込み) ---
ws.Range("A1").Value = "最適化なし (セル個別書き込み)"
StartHighResTimer
For r = 1 To NUM_ROWS
For c = 1 To NUM_COLS
ws.Cells(r + 1, c).Value = Rnd() ' 各セルに乱数を書き込み
Next c
DoEvents ' UI応答性を確保 (ただし性能は低下)
Next r
Debug.Print "最適化なし (セル個別書き込み) 実行時間: " & StopHighResTimer & " 秒"
ws.Range("B1").Value = StopHighResTimer & " 秒"
ws.Cells.ClearContents ' 再度シートをクリア
Application.Wait Now + TimeValue("00:00:01") ' 1秒待機
' --- 最適化あり (配列バッファ + ScreenUpdating/Calculation制御) ---
ws.Range("A1").Value = "最適化あり (配列バッファ + 制御)"
Application.ScreenUpdating = False ' 画面更新を停止
Application.Calculation = xlCalculationManual ' 自動計算を停止 (必要であれば)
Application.EnableEvents = False ' イベントを停止 (必要であれば)
ReDim dataArray(1 To NUM_ROWS, 1 To NUM_COLS) ' 配列を初期化
For r = 1 To NUM_ROWS
For c = 1 To NUM_COLS
dataArray(r, c) = Rnd() ' 配列に乱数を格納
Next c
Next r
StartHighResTimer
ws.Range("A2").Resize(NUM_ROWS, NUM_COLS).Value = dataArray ' 配列を一括書き込み
Debug.Print "最適化あり (配列バッファ + 制御) 実行時間: " & StopHighResTimer & " 秒"
ws.Range("B1").Value = StopHighResTimer & " 秒"
' --- 設定を元に戻す ---
Application.ScreenUpdating = True
Application.Calculation = xlCalculationAutomatic
Application.EnableEvents = True
MsgBox "Excelの性能テストが完了しました。", vbInformation, "テスト完了"
End Sub
</pre>
<p><strong>性能チューニングの数値例(実行環境により変動):</strong></p>
<figure class="wp-block-table"><table>
<thead>
<tr>
<th style="text-align:left;">処理内容</th>
<th style="text-align:left;">最適化なし</th>
<th style="text-align:left;">最適化あり (配列バッファ、画面更新停止など)</th>
<th style="text-align:left;">改善率 (相対比)</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:left;">10,000行 x 10列 のデータ書き込み (秒)</td>
<td style="text-align:left;">約 30.000 秒</td>
<td style="text-align:left;">約 0.050 秒</td>
<td style="text-align:left;">約 600倍</td>
</tr>
<tr>
<td style="text-align:left;">備考</td>
<td style="text-align:left;"><code>DoEvents</code>を含めています</td>
<td style="text-align:left;"><code>Application.ScreenUpdating = False</code>など</td>
<td style="text-align:left;"></td>
</tr>
</tbody>
</table></figure>
<h3 class="wp-block-heading">コード例2: Accessでのデータ処理と性能チューニング</h3>
<p>Accessで大量のレコードをテーブルに挿入する際の性能を、高精度タイマーで計測し、トランザクション処理による最適化の効果を数値で示します。</p>
<p><strong>実行手順:</strong></p>
<ol class="wp-block-list">
<li><p>上記「Win32 API宣言」と「タイマーヘルパー関数」のコードを標準モジュールに貼り付けます。</p></li>
<li><p>以下のコードを別の標準モジュールに貼り付けます。</p></li>
<li><p>Accessファイル内に<code>tblPerformanceTest</code>という名前のテーブルを作成します。</p>
<ul>
<li>フィールド: <code>ID</code> (オートナンバー、主キー), <code>Value</code> (テキスト型, 50), <code>Timestamp</code> (日付/時刻型)</li>
</ul></li>
<li><p><code>TestAccessPerformance</code>を実行します。イミディエイトウィンドウに結果が表示されます。</p></li>
</ol>
<pre data-enlighter-language="generic">' モジュール名: Module2 (または任意の標準モジュール)
Sub TestAccessPerformance()
Const NUM_RECORDS As Long = 10000 ' 処理対象のレコード数
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim startTime As Double
Dim i As Long
Set db = CurrentDb
' テーブルが存在しない場合は作成 (初回のみ実行)
On Error Resume Next
db.Execute "DROP TABLE tblPerformanceTest;", dbFailOnError
On Error GoTo 0
db.Execute "CREATE TABLE tblPerformanceTest (ID COUNTER PRIMARY KEY, Value TEXT(50), Timestamp DATETIME);", dbFailOnError
Debug.Print "--- Access 性能テスト開始 (" & Format(Date, "YYYY/MM/DD") & ") ---"
' --- 最適化なし (レコードごとの挿入) ---
db.Execute "DELETE FROM tblPerformanceTest;", dbFailOnError ' データクリア
Debug.Print "最適化なし (レコード個別挿入) を開始..."
StartHighResTimer
Set rs = db.OpenRecordset("tblPerformanceTest", dbOpenDynaset, dbAppendOnly)
For i = 1 To NUM_RECORDS
rs.AddNew
rs!Value = "TestValue " & i
rs!Timestamp = Now()
rs.Update
Next i
rs.Close
Set rs = Nothing
Debug.Print "最適化なし (レコード個別挿入) 実行時間: " & StopHighResTimer & " 秒 (" & NUM_RECORDS & "件)"
' --- 最適化あり (トランザクション処理) ---
db.Execute "DELETE FROM tblPerformanceTest;", dbFailOnError ' データクリア
Debug.Print "最適化あり (トランザクション処理) を開始..."
StartHighResTimer
db.BeginTrans ' トランザクション開始
Set rs = db.OpenRecordset("tblPerformanceTest", dbOpenDynaset, dbAppendOnly)
For i = 1 To NUM_RECORDS
rs.AddNew
rs!Value = "OptimizedValue " & i
rs!Timestamp = Now()
rs.Update
Next i
rs.Close
Set rs = Nothing
db.CommitTrans ' トランザクションコミット
Debug.Print "最適化あり (トランザクション処理) 実行時間: " & StopHighResTimer & " 秒 (" & NUM_RECORDS & "件)"
Set db = Nothing
MsgBox "Accessの性能テストが完了しました。イミディエイトウィンドウを確認してください。", vbInformation, "テスト完了"
End Sub
</pre>
<p><strong>性能チューニングの数値例(実行環境により変動):</strong></p>
<figure class="wp-block-table"><table>
<thead>
<tr>
<th style="text-align:left;">処理内容</th>
<th style="text-align:left;">最適化なし</th>
<th style="text-align:left;">最適化あり (トランザクション処理)</th>
<th style="text-align:left;">改善率 (相対比)</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:left;">10,000件のレコード挿入 (DAO/秒)</td>
<td style="text-align:left;">約 10.000 秒</td>
<td style="text-align:left;">約 0.100 秒</td>
<td style="text-align:left;">約 100倍</td>
</tr>
<tr>
<td style="text-align:left;">備考</td>
<td style="text-align:left;">レコードごとの更新</td>
<td style="text-align:left;"><code>BeginTrans</code>/<code>CommitTrans</code>を使用</td>
<td style="text-align:left;"></td>
</tr>
</tbody>
</table></figure>
<h2 class="wp-block-heading">検証</h2>
<p>実装したタイマーの精度と、実際の処理におけるオーバーヘッドを検証します。</p>
<ol class="wp-block-list">
<li><p><strong>精度比較</strong>:</p>
<ul>
<li><p><code>QueryPerformanceCounter</code>は、多くのシステムでマイクロ秒単位、場合によってはナノ秒単位の解像度を提供します。これは<code>QueryPerformanceFrequency</code>から得られる値で確認できます(例: 2.7GHzのCPUでは27億/秒)。</p></li>
<li><p><code>timeGetTime</code>は、<code>timeBeginPeriod(1)</code>を設定することで1ミリ秒の解像度を持ちます。これを設定しない場合のデフォルト解像度は、OSやハードウェアによって異なりますが、約10~16ミリ秒が一般的です。</p></li>
<li><p>VBAの<code>Timer</code>関数は秒単位のため、これらのAPIとは比較になりません。</p></li>
</ul></li>
<li><p><strong>オーバーヘッド測定</strong>:
タイマー自身の呼び出しにかかる時間を測定することで、タイマーのオーバーヘッドを把握できます。</p>
<pre data-enlighter-language="generic">Sub MeasureTimerOverhead()
Dim dblTime As Double
Dim i As Long
Const ITERATIONS As Long = 1000000 ' 繰り返し回数
Debug.Print "--- タイマーオーバーヘッド測定 ---"
' QueryPerformanceCounter のオーバーヘッド
StartHighResTimer
For i = 1 To ITERATIONS
' 何もしないループ
Next i
dblTime = StopHighResTimer
Debug.Print "QPCループ (" & ITERATIONS & "回) オーバーヘッド: " & dblTime & " 秒"
Debug.Print "QPC 1回あたりのオーバーヘッド: " & (dblTime / ITERATIONS * 1000000) & " マイクロ秒"
' timeGetTime のオーバーヘッド
StartMillisecondTimer
For i = 1 To ITERATIONS
' 何もしないループ
Next i
dblTime = StopMillisecondTimer
Debug.Print "timeGetTimeループ (" & ITERATIONS & "回) オーバーヘッド: " & dblTime & " 秒"
Debug.Print "timeGetTime 1回あたりのオーバーヘッド: " & (dblTime / ITERATIONS * 1000000) & " マイクロ秒"
' Timer 関数のオーバーヘッド
Dim t As Single
t = Timer
For i = 1 To ITERATIONS
' 何もしないループ
Next i
dblTime = Timer - t
Debug.Print "Timerループ (" & ITERATIONS & "回) オーバーヘッド: " & dblTime & " 秒"
Debug.Print "Timer 1回あたりのオーバーヘッド: " & (dblTime / ITERATIONS * 1000000) & " マイクロ秒"
End Sub
</pre>
<p><strong>測定結果例 (環境により変動):</strong></p>
<ul>
<li><p>QPC 1回あたりのオーバーヘッド: 約 0.05 – 0.2 マイクロ秒</p></li>
<li><p>timeGetTime 1回あたりのオーバーヘッド: 約 0.1 – 0.5 マイクロ秒</p></li>
<li><p>Timer 1回あたりのオーバーヘッド: 約 0.5 – 2 マイクロ秒</p></li>
</ul>
<p>これらの値は非常に小さく、通常、測定対象の処理時間に対して無視できるレベルです。しかし、非常に短い(数マイクロ秒以下)の処理を計測する場合、タイマー自身のオーバーヘッドが結果に影響を与える可能性があることに留意してください。</p></li>
</ol>
<h2 class="wp-block-heading">運用</h2>
<p>Win32 APIを用いた高精度タイマーを運用する上で、以下の点に注意してください。</p>
<ul class="wp-block-list">
<li><p><strong><code>timeBeginPeriod</code>と<code>timeEndPeriod</code>のペアリング</strong>: <code>timeBeginPeriod</code>でシステムタイマーの解像度を変更した場合、必ず対応する<code>timeEndPeriod</code>で元に戻す必要があります。これを怠ると、システム全体のリソースが不要に消費され、他のアプリケーションのパフォーマンスに影響を与える可能性があります。特に、エラー発生時にも<code>timeEndPeriod</code>が呼ばれるように、エラーハンドラ内に記述することが重要です。</p></li>
<li><p><strong>エラーハンドリング</strong>: API呼び出しは失敗する可能性があります(例: メモリ不足、無効なパラメータ)。<code>QueryPerformanceCounter</code>や<code>timeBeginPeriod</code>は、成功した場合は0以外の値を、失敗した場合は0を返すことが多いです。コード例では簡単なエラーチェックを行っていますが、実運用ではより堅牢なエラー処理を検討してください。</p></li>
<li><p><strong>VBA7 (64ビット) と VBA6 (32ビット) の互換性</strong>: 提示した<code>Declare PtrSafe</code>のコードは、<code>#If VBA7 Then</code>ディレクティブを使用しており、異なるビット数のOffice環境に対応しています。新しいOffice環境では<code>PtrSafe</code>が必須です。</p></li>
<li><p><strong>ロールバック方法</strong>:</p>
<ul>
<li><p>標準モジュールに貼り付けたコードを削除するだけです。特別な設定やシステム変更は行っていません。</p></li>
<li><p><code>timeBeginPeriod</code>を呼び出した場合は、<code>timeEndPeriod</code>が呼び出される前にアプリケーションがクラッシュすると、システムタイマーの解像度が変更されたままになる可能性があります。この場合、PCを再起動することでリセットされます。</p></li>
</ul></li>
</ul>
<h2 class="wp-block-heading">落とし穴</h2>
<ol class="wp-block-list">
<li><p><strong>API宣言のビット数と型</strong>: 32ビット版Officeと64ビット版Officeでは、API関数の引数や戻り値の型が変わる場合があります。特にポインタやハンドルを扱う場合は<code>LongPtr</code>を使用する必要があります。本記事では<code>Currency</code>型を64ビット整数として使用することで、<code>QueryPerformanceCounter</code>の64ビットカウンタ値に対応しています。<code>Long</code>型は32ビットなので、64ビット値を扱う際にはオーバーフローする可能性があります。</p></li>
<li><p><strong><code>timeBeginPeriod</code>のリソースリーク</strong>: 前述の通り、<code>timeBeginPeriod</code>を呼び出した後、対応する<code>timeEndPeriod</code>を呼び出さないと、システムのタイマー解像度が高いまま維持され、CPUが消費電力を多く使う、バッテリーの持ちが悪くなるなどの影響が出ることがあります。必ずペアで使用し、エラー発生時も適切に<code>timeEndPeriod</code>を呼び出すように設計してください。</p></li>
<li><p><strong>コンテキストスイッチとCPUキャッシュ</strong>: 高精度タイマーはOSやCPUの低レベルな動作に影響されます。特に、処理中に他のプロセスへのコンテキストスイッチが発生したり、CPUキャッシュがクリアされたりすると、計測値にばらつきが生じることがあります。非常に短い処理(数マイクロ秒以下)を計測する場合は、複数回実行して平均値を取るなどの工夫が必要です。</p></li>
<li><p><strong>Windowsの電源管理による周波数変動</strong>: 一部のシステムでは、CPUの省電力機能によりクロック周波数が動的に変化することがあります。<code>QueryPerformanceCounter</code>は通常、この変動に影響を受けないように設計されていますが、古いシステムや特定のハードウェア構成では注意が必要です。</p></li>
</ol>
<h2 class="wp-block-heading">まとめ</h2>
<p>VBAでミリ秒以下の精度で処理時間を計測するには、<code>QueryPerformanceCounter</code>や<code>timeGetTime</code>といったWin32 APIの活用が不可欠です。</p>
<ul class="wp-block-list">
<li><p><strong><code>QueryPerformanceCounter</code></strong>は、マイクロ秒〜ナノ秒単位の最高の精度を提供し、性能ベンチマークやクリティカルな時間計測に最適です。</p></li>
<li><p><strong><code>timeGetTime</code></strong>は、<code>timeBeginPeriod</code>との組み合わせでミリ秒単位の精度を提供し、比較的シンプルに利用できますが、リソース管理に注意が必要です。</p></li>
</ul>
<p>また、ExcelやAccessのようなOfficeアプリケーションでの大量データ処理では、<strong>配列バッファの利用、<code>Application.ScreenUpdating</code>や<code>Application.Calculation</code>の制御、データベーストランザクションの活用</strong>といったVBA固有の性能チューニング手法を組み合わせることで、処理時間を劇的に短縮できます。高精度タイマーは、これらの最適化の効果を定量的に評価するための強力なツールとなります。</p>
<p>Win32 APIを適切に利用することで、VBAアプリケーションの可能性を広げ、より高度な要件に対応できるようになります。
本記事では2024年7月30日時点の情報に基づいて解説しています。参照したMicrosoft Learnのドキュメントは、QueryPerformanceCounter/QueryPerformanceFrequencyが2023年8月2日に更新されていますが、timeGetTimeやGetTickCount64は2018年12月5日の更新が最終です。これらのAPIの基本的な機能は長期にわたって安定しています。</p>
<hr/>
<p>[1] Microsoft. (2023年8月2日). QueryPerformanceCounter function. Microsoft Learn. <a href="https://learn.microsoft.com/en-us/windows/win32/api/profileapi/nf-profileapi-queryperformancecounter">https://learn.microsoft.com/en-us/windows/win32/api/profileapi/nf-profileapi-queryperformancecounter</a>
[2] Microsoft. (2023年8月2日). QueryPerformanceFrequency function. Microsoft Learn. <a href="https://learn.microsoft.com/en-us/windows/win32/api/profileapi/nf-profileapi-queryperformancefrequency">https://learn.microsoft.com/en-us/windows/win32/api/profileapi/nf-profileapi-queryperformancefrequency</a>
[3] Microsoft. (2018年12月5日). timeGetTime function. Microsoft Learn. <a href="https://learn.microsoft.com/en-us/windows/win32/api/mmsystem/nf-mmsystem-timegettime">https://learn.microsoft.com/en-us/windows/win32/api/mmsystem/nf-mmsystem-timegettime</a>
[4] Microsoft. (2018年12月5日). timeBeginPeriod function. Microsoft Learn. <a href="https://learn.microsoft.com/en-us/windows/win32/api/mmsystem/nf-mmsystem-timebeginperiod">https://learn.microsoft.com/en-us/windows/win32/api/mmsystem/nf-mmsystem-timebeginperiod</a>
[5] Microsoft. (2018年12月5日). GetTickCount64 function. Microsoft Learn. <a href="https://learn.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-gettickcount64">https://learn.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-gettickcount64</a></p>
本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証) です。
VBAでWin32 APIを用いた高精度タイマーの実装と最適化
背景と要件
VBAにおける処理時間の計測には、通常、組み込みのTimer関数が使用されます。しかし、Timer関数は秒単位の精度しか持たず、ミリ秒以下の高精度な時間計測には不向きです。特に、以下のようなシナリオでは、より高精度なタイマーが求められます。
性能測定とボトルネック特定 : 複雑なVBAコードの実行時間を詳細に分析し、性能改善の余地がある部分を特定する。
リアルタイム処理 : シミュレーションや外部デバイスとの連携など、厳密な時間管理が必要な処理。
細かなチューニング効果の検証 : コードの最適化前後で、微細な性能向上を数値で確認する。
この要件を満たすため、本記事ではWindowsの低レベルな機能を提供するWin32 API をVBAから利用し、高精度なタイマーを実装する方法について解説します。外部ライブラリに依存せず、標準機能のみで実現することを目標とします。
設計
高精度タイマーの選定
Win32 APIには複数の時間計測関数が存在しますが、精度と用途によって使い分けが必要です。
QueryPerformanceCounter / QueryPerformanceFrequency [1, 2] :
timeGetTime [3] :
GetTickCount64 [5] :
比較表:
タイマー関数
精度
特徴
利点
欠点
Timer (VBA組み込み)
1秒
VBA標準。
手軽に利用可能。
精度が低く、短時間計測には不向き。
GetTickCount64
約10-16ミリ秒
システム起動からのミリ秒。64ビット。
オーバーフローの心配が少ない。
精度が低く、timeGetTimeより汎用性で劣る。
timeGetTime
約10-16ミリ秒 (デフォルト) 1ミリ秒 (timeBeginPeriod併用時)
マルチメディアタイマー。システム全体のタイマー解像度を調整可能。
比較的シンプル。ミリ秒精度が必要な場合に有効。
timeBeginPeriodとtimeEndPeriodのペアリングが必須。システムリソースを消費。
QueryPerformanceCounter
マイクロ秒〜ナノ秒
ハードウェアの高性能カウンタを使用。
最高の精度。
QueryPerformanceFrequencyで正規化が必要。オーバーヘッドがごくわずかに存在。
、最高の精度を提供するQueryPerformanceCounter をメインに、汎用性の高いtimeGetTime を比較対象として取り上げます。
処理フロー
タイマーを利用した処理の一般的な流れを以下に示します。
graph TD
A["VBAプロシージャ開始"] --> B{"タイマー初期化"};
B --> C{"Win32 API選択"};
C -- QueryPerformanceCounter を使用 --> D["QPC_StartCount 取得"];
C -- timeGetTime を使用 --> E["TimeGetTime_Start 取得"];
D --> F["実行対象のVBAコード"];
E --> F;
F --> G{"タイマー終了"};
G -- QueryPerformanceCounter を使用 --> H["QPC_EndCount 取得"];
G -- timeGetTime を使用 --> I["TimeGetTime_End 取得"];
H --> J["時間差を計算 (QPC_EndCount - QPC_StartCount) / QPC_Frequency"];
I --> K["時間差を計算 (TimeGetTime_End - TimeGetTime_Start) / 1000#"];
J --> L["結果表示またはログ記録"];
K --> L;
L --> M["VBAプロシージャ終了"];
実装
VBAでWin32 APIを使用するには、Declare PtrSafeステートメントを使ってAPI関数を宣言する必要があります。ExcelやAccessのVBAエディタで、標準モジュールに以下のコードを記述します。
Win32 API宣言
' このコードは、VBA7 (64ビットOffice) およびVBA6 (32ビットOffice) の両方に対応しています。
' PtrSafe は VBA7 での64ビット互換性確保のために必要です。
' === QueryPerformanceCounter (高精度タイマー) ===
' lpPerformanceCount: 現在のカウンタ値
' lpFrequency: カウンタの周波数
#If VBA7 Then ' Office 2010以降 (64ビット版を含む)
Private Declare PtrSafe Function QueryPerformanceCounter Lib "kernel32" (lpPerformanceCount As Currency) As Long
Private Declare PtrSafe Function QueryPerformanceFrequency Lib "kernel32" (lpFrequency As Currency) As Long
#Else ' Office 2007以前 (32ビット版のみ)
Private Declare Function QueryPerformanceCounter Lib "kernel32" (lpPerformanceCount As Any) As Long
Private Declare Function QueryPerformanceFrequency Lib "kernel32" (lpFrequency As Any) As Long
#End If
' === timeGetTime (マルチメディアタイマー) ===
' timeGetTime: システム時刻をミリ秒で取得
' timeBeginPeriod: システムタイマーの解像度を設定
' timeEndPeriod: システムタイマーの解像度を元に戻す
#If VBA7 Then
Private Declare PtrSafe Function timeGetTime Lib "winmm.dll" () As Long
Private Declare PtrSafe Function timeBeginPeriod Lib "winmm.dll" (ByVal uPeriod As Long) As Long
Private Declare PtrSafe Function timeEndPeriod Lib "winmm.dll" (ByVal uPeriod As Long) As Long
#Else
Private Declare Function timeGetTime Lib "winmm.dll" () As Long
Private Declare Function timeBeginPeriod Lib "winmm.dll" (ByVal uPeriod As Long) As Long
Private Declare Function timeEndPeriod Lib "winmm.dll" (ByVal uPeriod As Long) As Long
#End If
' === グローバル変数 ===
Private s_QPC_StartCount As Currency ' QueryPerformanceCounterの開始カウンタ値
Private s_QPC_Frequency As Currency ' QueryPerformanceCounterの周波数
Private s_timeGetTime_Start As Long ' timeGetTimeの開始ミリ秒
Private s_timeGetTime_PeriodSet As Boolean ' timeBeginPeriodが設定されたか
タイマーヘルパー関数
上記のAPIを使って、タイマーを簡単に利用するためのヘルパー関数を作成します。
' QueryPerformanceCounter を使った高精度タイマーの開始
Public Sub StartHighResTimer()
' 周波数を一度だけ取得
If s_QPC_Frequency = 0 Then
If QueryPerformanceFrequency(s_QPC_Frequency) = 0 Then
Err.Raise vbObjectError + 1000, "StartHighResTimer", "QueryPerformanceFrequency API呼び出しに失敗しました。"
End If
End If
' 開始カウンタ値を取得
If QueryPerformanceCounter(s_QPC_StartCount) = 0 Then
Err.Raise vbObjectError + 1001, "StartHighResTimer", "QueryPerformanceCounter API呼び出しに失敗しました。"
End If
End Sub
' QueryPerformanceCounter を使った高精度タイマーの終了 (秒単位で結果を返す)
Public Function StopHighResTimer() As Double
Dim EndCount As Currency
If QueryPerformanceCounter(EndCount) = 0 Then
Err.Raise vbObjectError + 1002, "StopHighResTimer", "QueryPerformanceCounter API呼び出しに失敗しました。"
End If
' (終了カウンタ - 開始カウンタ) / 周波数 = 経過秒数
StopHighResTimer = CDbl((EndCount - s_QPC_StartCount) / s_QPC_Frequency)
End Function
' timeGetTime を使ったミリ秒タイマーの開始 (解像度を1msに設定)
Public Sub StartMillisecondTimer()
' システムタイマーの解像度を1msに設定
' 失敗しても続行可能だが、精度はシステムデフォルトになる
If timeBeginPeriod(1) = 0 Then
s_timeGetTime_PeriodSet = True
Else
Debug.Print "警告: timeBeginPeriod(1) の設定に失敗しました。システムデフォルトの解像度を使用します。"
s_timeGetTime_PeriodSet = False
End If
s_timeGetTime_Start = timeGetTime()
End Sub
' timeGetTime を使ったミリ秒タイマーの終了 (秒単位で結果を返す)
Public Function StopMillisecondTimer() As Double
Dim EndTime As Long
EndTime = timeGetTime()
' timeBeginPeriodで設定した解像度を元に戻す
If s_timeGetTime_PeriodSet Then
timeEndPeriod 1
s_timeGetTime_PeriodSet = False
End If
StopMillisecondTimer = CDbl(EndTime - s_timeGetTime_Start) / 1000# ' ミリ秒を秒に変換
End Function
コード例1: Excelでのデータ処理と性能チューニング
Excelで大量のセルにデータを書き込む際の性能を、高精度タイマーで計測し、配列バッファや画面更新制御による最適化の効果を数値で示します。
実行手順:
上記「Win32 API宣言」と「タイマーヘルパー関数」のコードを標準モジュールに貼り付けます。
以下のコードを別の標準モジュールに貼り付けます。
TestExcelPerformanceを実行します。シート1に結果が表示されます。
' モジュール名: Module1 (または任意の標準モジュール)
Sub TestExcelPerformance()
Const NUM_ROWS As Long = 10000 ' 処理対象の行数
Const NUM_COLS As Long = 10 ' 処理対象の列数
Dim ws As Worksheet
Dim r As Long, c As Long
Dim startTime As Double
Dim dataArray() As Variant
Dim i As Long
Set ws = ThisWorkbook.Sheets(1)
ws.Cells.ClearContents ' シートをクリア
' --- 最適化なし (セルごとの書き込み) ---
ws.Range("A1").Value = "最適化なし (セル個別書き込み)"
StartHighResTimer
For r = 1 To NUM_ROWS
For c = 1 To NUM_COLS
ws.Cells(r + 1, c).Value = Rnd() ' 各セルに乱数を書き込み
Next c
DoEvents ' UI応答性を確保 (ただし性能は低下)
Next r
Debug.Print "最適化なし (セル個別書き込み) 実行時間: " & StopHighResTimer & " 秒"
ws.Range("B1").Value = StopHighResTimer & " 秒"
ws.Cells.ClearContents ' 再度シートをクリア
Application.Wait Now + TimeValue("00:00:01") ' 1秒待機
' --- 最適化あり (配列バッファ + ScreenUpdating/Calculation制御) ---
ws.Range("A1").Value = "最適化あり (配列バッファ + 制御)"
Application.ScreenUpdating = False ' 画面更新を停止
Application.Calculation = xlCalculationManual ' 自動計算を停止 (必要であれば)
Application.EnableEvents = False ' イベントを停止 (必要であれば)
ReDim dataArray(1 To NUM_ROWS, 1 To NUM_COLS) ' 配列を初期化
For r = 1 To NUM_ROWS
For c = 1 To NUM_COLS
dataArray(r, c) = Rnd() ' 配列に乱数を格納
Next c
Next r
StartHighResTimer
ws.Range("A2").Resize(NUM_ROWS, NUM_COLS).Value = dataArray ' 配列を一括書き込み
Debug.Print "最適化あり (配列バッファ + 制御) 実行時間: " & StopHighResTimer & " 秒"
ws.Range("B1").Value = StopHighResTimer & " 秒"
' --- 設定を元に戻す ---
Application.ScreenUpdating = True
Application.Calculation = xlCalculationAutomatic
Application.EnableEvents = True
MsgBox "Excelの性能テストが完了しました。", vbInformation, "テスト完了"
End Sub
性能チューニングの数値例(実行環境により変動):
処理内容
最適化なし
最適化あり (配列バッファ、画面更新停止など)
改善率 (相対比)
10,000行 x 10列 のデータ書き込み (秒)
約 30.000 秒
約 0.050 秒
約 600倍
備考
DoEventsを含めています
Application.ScreenUpdating = Falseなど
コード例2: Accessでのデータ処理と性能チューニング
Accessで大量のレコードをテーブルに挿入する際の性能を、高精度タイマーで計測し、トランザクション処理による最適化の効果を数値で示します。
実行手順:
上記「Win32 API宣言」と「タイマーヘルパー関数」のコードを標準モジュールに貼り付けます。
以下のコードを別の標準モジュールに貼り付けます。
Accessファイル内にtblPerformanceTestという名前のテーブルを作成します。
フィールド: ID (オートナンバー、主キー), Value (テキスト型, 50), Timestamp (日付/時刻型)
TestAccessPerformanceを実行します。イミディエイトウィンドウに結果が表示されます。
' モジュール名: Module2 (または任意の標準モジュール)
Sub TestAccessPerformance()
Const NUM_RECORDS As Long = 10000 ' 処理対象のレコード数
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim startTime As Double
Dim i As Long
Set db = CurrentDb
' テーブルが存在しない場合は作成 (初回のみ実行)
On Error Resume Next
db.Execute "DROP TABLE tblPerformanceTest;", dbFailOnError
On Error GoTo 0
db.Execute "CREATE TABLE tblPerformanceTest (ID COUNTER PRIMARY KEY, Value TEXT(50), Timestamp DATETIME);", dbFailOnError
Debug.Print "--- Access 性能テスト開始 (" & Format(Date, "YYYY/MM/DD") & ") ---"
' --- 最適化なし (レコードごとの挿入) ---
db.Execute "DELETE FROM tblPerformanceTest;", dbFailOnError ' データクリア
Debug.Print "最適化なし (レコード個別挿入) を開始..."
StartHighResTimer
Set rs = db.OpenRecordset("tblPerformanceTest", dbOpenDynaset, dbAppendOnly)
For i = 1 To NUM_RECORDS
rs.AddNew
rs!Value = "TestValue " & i
rs!Timestamp = Now()
rs.Update
Next i
rs.Close
Set rs = Nothing
Debug.Print "最適化なし (レコード個別挿入) 実行時間: " & StopHighResTimer & " 秒 (" & NUM_RECORDS & "件)"
' --- 最適化あり (トランザクション処理) ---
db.Execute "DELETE FROM tblPerformanceTest;", dbFailOnError ' データクリア
Debug.Print "最適化あり (トランザクション処理) を開始..."
StartHighResTimer
db.BeginTrans ' トランザクション開始
Set rs = db.OpenRecordset("tblPerformanceTest", dbOpenDynaset, dbAppendOnly)
For i = 1 To NUM_RECORDS
rs.AddNew
rs!Value = "OptimizedValue " & i
rs!Timestamp = Now()
rs.Update
Next i
rs.Close
Set rs = Nothing
db.CommitTrans ' トランザクションコミット
Debug.Print "最適化あり (トランザクション処理) 実行時間: " & StopHighResTimer & " 秒 (" & NUM_RECORDS & "件)"
Set db = Nothing
MsgBox "Accessの性能テストが完了しました。イミディエイトウィンドウを確認してください。", vbInformation, "テスト完了"
End Sub
性能チューニングの数値例(実行環境により変動):
処理内容
最適化なし
最適化あり (トランザクション処理)
改善率 (相対比)
10,000件のレコード挿入 (DAO/秒)
約 10.000 秒
約 0.100 秒
約 100倍
備考
レコードごとの更新
BeginTrans/CommitTransを使用
検証
実装したタイマーの精度と、実際の処理におけるオーバーヘッドを検証します。
精度比較 :
QueryPerformanceCounterは、多くのシステムでマイクロ秒単位、場合によってはナノ秒単位の解像度を提供します。これはQueryPerformanceFrequencyから得られる値で確認できます(例: 2.7GHzのCPUでは27億/秒)。
timeGetTimeは、timeBeginPeriod(1)を設定することで1ミリ秒の解像度を持ちます。これを設定しない場合のデフォルト解像度は、OSやハードウェアによって異なりますが、約10~16ミリ秒が一般的です。
VBAのTimer関数は秒単位のため、これらのAPIとは比較になりません。
オーバーヘッド測定 :
タイマー自身の呼び出しにかかる時間を測定することで、タイマーのオーバーヘッドを把握できます。
Sub MeasureTimerOverhead()
Dim dblTime As Double
Dim i As Long
Const ITERATIONS As Long = 1000000 ' 繰り返し回数
Debug.Print "--- タイマーオーバーヘッド測定 ---"
' QueryPerformanceCounter のオーバーヘッド
StartHighResTimer
For i = 1 To ITERATIONS
' 何もしないループ
Next i
dblTime = StopHighResTimer
Debug.Print "QPCループ (" & ITERATIONS & "回) オーバーヘッド: " & dblTime & " 秒"
Debug.Print "QPC 1回あたりのオーバーヘッド: " & (dblTime / ITERATIONS * 1000000) & " マイクロ秒"
' timeGetTime のオーバーヘッド
StartMillisecondTimer
For i = 1 To ITERATIONS
' 何もしないループ
Next i
dblTime = StopMillisecondTimer
Debug.Print "timeGetTimeループ (" & ITERATIONS & "回) オーバーヘッド: " & dblTime & " 秒"
Debug.Print "timeGetTime 1回あたりのオーバーヘッド: " & (dblTime / ITERATIONS * 1000000) & " マイクロ秒"
' Timer 関数のオーバーヘッド
Dim t As Single
t = Timer
For i = 1 To ITERATIONS
' 何もしないループ
Next i
dblTime = Timer - t
Debug.Print "Timerループ (" & ITERATIONS & "回) オーバーヘッド: " & dblTime & " 秒"
Debug.Print "Timer 1回あたりのオーバーヘッド: " & (dblTime / ITERATIONS * 1000000) & " マイクロ秒"
End Sub
測定結果例 (環境により変動):
QPC 1回あたりのオーバーヘッド: 約 0.05 – 0.2 マイクロ秒
timeGetTime 1回あたりのオーバーヘッド: 約 0.1 – 0.5 マイクロ秒
Timer 1回あたりのオーバーヘッド: 約 0.5 – 2 マイクロ秒
これらの値は非常に小さく、通常、測定対象の処理時間に対して無視できるレベルです。しかし、非常に短い(数マイクロ秒以下)の処理を計測する場合、タイマー自身のオーバーヘッドが結果に影響を与える可能性があることに留意してください。
運用
Win32 APIを用いた高精度タイマーを運用する上で、以下の点に注意してください。
timeBeginPeriodとtimeEndPeriodのペアリング : timeBeginPeriodでシステムタイマーの解像度を変更した場合、必ず対応するtimeEndPeriodで元に戻す必要があります。これを怠ると、システム全体のリソースが不要に消費され、他のアプリケーションのパフォーマンスに影響を与える可能性があります。特に、エラー発生時にもtimeEndPeriodが呼ばれるように、エラーハンドラ内に記述することが重要です。
エラーハンドリング : API呼び出しは失敗する可能性があります(例: メモリ不足、無効なパラメータ)。QueryPerformanceCounterやtimeBeginPeriodは、成功した場合は0以外の値を、失敗した場合は0を返すことが多いです。コード例では簡単なエラーチェックを行っていますが、実運用ではより堅牢なエラー処理を検討してください。
VBA7 (64ビット) と VBA6 (32ビット) の互換性 : 提示したDeclare PtrSafeのコードは、#If VBA7 Thenディレクティブを使用しており、異なるビット数のOffice環境に対応しています。新しいOffice環境ではPtrSafeが必須です。
ロールバック方法 :
落とし穴
API宣言のビット数と型 : 32ビット版Officeと64ビット版Officeでは、API関数の引数や戻り値の型が変わる場合があります。特にポインタやハンドルを扱う場合はLongPtrを使用する必要があります。本記事ではCurrency型を64ビット整数として使用することで、QueryPerformanceCounterの64ビットカウンタ値に対応しています。Long型は32ビットなので、64ビット値を扱う際にはオーバーフローする可能性があります。
timeBeginPeriodのリソースリーク : 前述の通り、timeBeginPeriodを呼び出した後、対応するtimeEndPeriodを呼び出さないと、システムのタイマー解像度が高いまま維持され、CPUが消費電力を多く使う、バッテリーの持ちが悪くなるなどの影響が出ることがあります。必ずペアで使用し、エラー発生時も適切にtimeEndPeriodを呼び出すように設計してください。
コンテキストスイッチとCPUキャッシュ : 高精度タイマーはOSやCPUの低レベルな動作に影響されます。特に、処理中に他のプロセスへのコンテキストスイッチが発生したり、CPUキャッシュがクリアされたりすると、計測値にばらつきが生じることがあります。非常に短い処理(数マイクロ秒以下)を計測する場合は、複数回実行して平均値を取るなどの工夫が必要です。
Windowsの電源管理による周波数変動 : 一部のシステムでは、CPUの省電力機能によりクロック周波数が動的に変化することがあります。QueryPerformanceCounterは通常、この変動に影響を受けないように設計されていますが、古いシステムや特定のハードウェア構成では注意が必要です。
まとめ
VBAでミリ秒以下の精度で処理時間を計測するには、QueryPerformanceCounterやtimeGetTimeといったWin32 APIの活用が不可欠です。
また、ExcelやAccessのようなOfficeアプリケーションでの大量データ処理では、配列バッファの利用、Application.ScreenUpdatingやApplication.Calculationの制御、データベーストランザクションの活用 といったVBA固有の性能チューニング手法を組み合わせることで、処理時間を劇的に短縮できます。高精度タイマーは、これらの最適化の効果を定量的に評価するための強力なツールとなります。
Win32 APIを適切に利用することで、VBAアプリケーションの可能性を広げ、より高度な要件に対応できるようになります。
本記事では2024年7月30日時点の情報に基づいて解説しています。参照したMicrosoft Learnのドキュメントは、QueryPerformanceCounter/QueryPerformanceFrequencyが2023年8月2日に更新されていますが、timeGetTimeやGetTickCount64は2018年12月5日の更新が最終です。これらのAPIの基本的な機能は長期にわたって安定しています。
[1] Microsoft. (2023年8月2日). QueryPerformanceCounter function. Microsoft Learn. https://learn.microsoft.com/en-us/windows/win32/api/profileapi/nf-profileapi-queryperformancecounter
[2] Microsoft. (2023年8月2日). QueryPerformanceFrequency function. Microsoft Learn. https://learn.microsoft.com/en-us/windows/win32/api/profileapi/nf-profileapi-queryperformancefrequency
[3] Microsoft. (2018年12月5日). timeGetTime function. Microsoft Learn. https://learn.microsoft.com/en-us/windows/win32/api/mmsystem/nf-mmsystem-timegettime
[4] Microsoft. (2018年12月5日). timeBeginPeriod function. Microsoft Learn. https://learn.microsoft.com/en-us/windows/win32/api/mmsystem/nf-mmsystem-timebeginperiod
[5] Microsoft. (2018年12月5日). GetTickCount64 function. Microsoft Learn. https://learn.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-gettickcount64
コメント