【テクニカル・上級編】配列の動的再定義(ReDim)コストを最小化するバッファ戦略 – Excel VBA解析バイブル

スポンサーリンク

配列の動的再定義(ReDim)コストを最小化するバッファ戦略

VBA(Visual Basic for Applications)の現場において、未だに「レコード数やデータ件数が事前に分からないから」という理由で、ループの内部で毎度 `ReDim Preserve` を実行しているコードを見かける。

‘ 【アンチパターン】これぞVBAのパフォーマンスを殺す最も手っ取り早い方法
Dim data() As String
Dim i As Long
For i = 1 To 100000
ReDim Preserve data(1 i)
data(i) = “Value ” & i
Next i

シニアエンジニアであれば、このコードが動くからといって放置してはならない。なぜなら、この記述はO(N^2)の計算量を隠蔽した最悪のメモリ破壊劇だからだ。

今回は、VBAにおけるメモリの挙動、そしてOSの深層における動的配列の再割り当てコストを直視し、極限までパフォーマンスを研ぎ澄ます「バッファ戦略(指数関数的プレアロケーション)」の全貌を解説する。

1. なぜ `ReDim Preserve` をループ内で使うとシステムが崩壊するのか

VBAの配列は、実体の多くがCOM(Component Object Model)のセーフアレイ(`SAFEARRAY`構造体)としてヒープ上に確保される。

ループ内で `ReDim Preserve` を呼ぶたびに、VBAのランタイム(`VBA6.DLL` 等)は以下の過酷な処理を水面下で実行している。

1. 新規メモリ領域の確保: 現在のサイズ + 1 の新しい連続したメモリブロックをヒープ上に確保する。
2. 既存データのコピー: 旧領域から新領域へ、全要素のメモリコピー(`memcpy`)を行う。
3. 旧領域の解放: 古いメモリブロックを破棄する。

これをN回繰り返す。結果として発生するコピーの総量は $1 + 2 + 3 + \dots + N = \frac{N(N+1)}{2}$ となり、処理時間は要素数に対して二乗比例で爆発する。10万件のデータをこれで処理させれば、モダンなCPUであってもファンが唸り、プロセスはフリーズしたかのような沈黙に陥る。

2. 幾何学的成長(Geometric Growth)によるバッファ戦略

この問題を解決する唯一にして最大の解法が、「バッファ戦略(事前割り当てと指数的拡張)」である。

これは、データ構造の基本アルゴリズム(C++の `std::vector` や Javaの `ArrayList` の内部実装)をVBAに持ち込むアプローチだ。

戦略のアルゴリズム

1. 予測される最大サイズ、あるいは十分な初期サイズの配列を最初に確保する。
2. 配列が満杯になったら、要素数を「1つずつ」ではなく、「現在の容量の2倍(あるいは1.5倍)」に拡張する。
3. 実際に有効なデータ件数を管理するポインタ(インデックス)を別に保持し、最終的な出力時のみ、正確なサイズに `ReDim`(縮小)する。

この戦略により、`ReDim` の実行回数は $N$ 回から $\log_2 N$ 回へと劇的に激減し、計算量は O(N) へと回収される。

3. 実装コード:限界まで最適化された動的バッファクラス

実務でそのままコピー&ペーストして使える、高速配列ビルダーのモジュールコードを提示する。ここではクラスモジュールを使用せず、手続き的な美しさとオーバーヘッドの排除を両立させた標準モジュールのパターンで示す。

Option Explicit

‘ =====================================================================
‘ 圧倒的なパフォーマンスを誇る動的配列バッファハンドラ
‘ =====================================================================
Public Sub ExecuteHighPerformanceBuffer()
Dim sw As Double
sw = Timer

‘ 1. バッファ変数の定義
Dim buffer() As String
Dim capacity As Long ‘ 現在確保しているメモリの最大容量
Dim count As Long ‘ 実際に格納されたデータの件数

‘ 初期容量の確保(例: 1024要素からスタート)
capacity = 1024
count = 0
ReDim buffer(0 To capacity – 1)

‘ 2. 模擬データ挿入ループ(10万件)
Dim i As Long
For i = 1 To 100000
‘ 容量が溢れた場合の拡張処理(倍加戦略)
If count >= capacity Then
capacity = capacity 2
ReDim Preserve buffer(0 To capacity – 1)
End If

‘ データの格納とカウンタのインクリメント
buffer(count) = “Data_Index_” & i
count = count + 1
Next i

‘ 3. 最終的なデータ数にトリミング(無駄なメモリを切り捨てる)
If count > 0 Then
ReDim Preserve buffer(0 To count – 1)
Else
Erase buffer
End If

Debug.Print “処理完了: ” & count & ” 件 / 実行時間: ” & Format(Timer – sw, “0.000秒”)

‘ ここ以降で buffer(index) を高速に処理…
End Sub

このコードの優位性

  • 再割り当て回数の激減: 10万件のデータに対し、`ReDim Preserve` はわずか 7回 しか実行されない(1024 → 2048 → 4096 → 8192 → 16384 → 32768 → 65536 → 131072)。
  • O(N) への収束: メモリコピーのオーバヘッドが極限まで削ぎ落とされ、数ミリ秒単位での処理が可能となる。

4. チーフアーキテクトが教える:さらに踏み込んだメモリ最適化の極意

実務システム、特に外部API連携や巨大なCSV/RDBからのデータバルクインポートにおいては、上記のバッファリングに加え、以下のアーキテクチャ的配慮が不可欠となる。

A. 巨大配列の破棄とメモリリークの防止

VBAのガベージコレクションは参照カウント方式をベースとしている。Variant型やオブジェクト型を格納する配列の場合、単に `ReDim` で縮小しても、内部のポインタが指すオブジェクトが即座に解放されないケースがある。
配列を使い捨てにする場合は、`Erase buffer` を明示的に呼び出し、セーフアレイ自体のヘッダーとヒープ領域を即座にOSへ返還させよ。

B. Windows API(`CopyMemory`)によるさらなる加速

文字列ではなく、構造体(UDT: User Defined Type)の巨大な配列を扱う場合、VBのランタイムを経由する代入処理すらボトルネックになることがある。
その場合は `kernel32.dll` の `RtlMoveMemory`(通称 `CopyMemory`)を宣言し、メモリブロックを一括コピーするアプローチをとる。

If VBA7 Then
Declare PtrSafe Sub RtlMoveMemory Lib “kernel32” (Destination As Any, Source As Any, ByVal Length As LongPtr)
Else
Declare Sub RtlMoveMemory Lib “kernel32” (Destination As Any, Source As Any, ByVal Length As Long)
End If

※ただし、ポインタ操作を誤ると一撃でExcelがクラッシュ(VBAの強制終了)するため、適用はシニアエンジニアのコードレビューを通過したものに限定すべきである。

総括

VBAは「おもちゃの言語」ではない。適切なアルゴリズムとメモリ管理の哲学を持って挑めば、CやC++で書かれたネイティブモジュールに匹敵するデータ処理能力を引き出すことができる。

ループ内での安易な `ReDim Preserve` は、VBAエンジニアとしての自尊心を捨て去る行為に等しい。
今日から「幾何学的成長によるバッファ戦略」を標準装備し、レガシーシステムの限界を軽々と突破してほしい。

タイトルとURLをコピーしました