動的配列の真髄:ReDimとPreserveが織りなすメモリ管理の極限最適化
VBAのコードベースにおいて、パフォーマンス低下の元凶の大半は「不適切なメモリ確保」と「セルへの無駄なアクセス」にある。
あらかじめ要素数が確定していないデータを扱う際、定跡として用いられるのが `ReDim` ステートメント、そして既存データを保持するための `Preserve` キーワードだ。
しかし、シニアエンジニアであれば誰もが知っている通り、`ReDim Preserve` を愚直にループ内で毎回実行するコードは、メモリの再割り当てとコピーの嵐を引き起こし、システムを確実に死に至らしめる。
本稿では、VBAにおける動的配列のライフサイクルを根本から見つめ直し、メモリの断片化を防ぎつつ、極限までパフォーマンスを引き出すための実装パターンを、チーフアーキテクトの視点から解説する。
—
1. 動的配列のメカニズムと `Preserve` のコスト
VBAの配列は、メモリ上の連続した領域に配置される。静的配列(`Dim arr(10) As Long` など)はコンパイル時にスタック(またはヒープ)上に固定領域が確保されるが、動的配列は `Dim arr() As Long` のように次元だけを宣言し、実行時に `ReDim` で領域をヒープ上に割り当てる。
ここで問題となるのが `Preserve` だ。
`ReDim Preserve arr(newUBound)` を実行すると、VBAの裏側では以下の処理が強制される。
1. 新しいサイズ(`newUBound`)に応じた新しい連続したメモリ領域をヒープ上に確保する。
2. 既存の配列が持つデータを、古い領域から新しい領域へバイナリレベルでコピーする。
3. 古いメモリ領域を解放する。
つまり、配列のサイズを $1$ 拡張するたびにこのコピーが発生する。要素数が数万件に達した場合、計算量は $O(N^2)$ に跳ね上がり、CPUはメモリのコピーだけにリソースを奪われることになる。
—
2. 【極限最適化】チャンク(倍加)アロケーション戦略
この問題に対するアーキテクチャ上の解答が、「チャンク(倍加)アロケーション戦略」だ。
メモリの再割り当て回数を対数オーダー($O(\log N)$)に抑えるため、必要サイズを超える余裕を持った領域(バッファ)をあらかじめ確保し、上限に達した時点でサイズを倍々に拡張していく。
以下に、実業務のシステム間連携や巨大なログ解析において耐えうる、堅牢かつ高速な動的配列管理クラスの模範コードを示す。
実装コード:高効率動動的配列マネージャー
Option Explicit
‘ =================================================================ay
‘ クラス名: FastDynamicArray
‘ 概要: 巨大データの取り込みにおいて、ReDim Preserveのオーバーヘッドを
‘ 極限まで排除するためのチャンクアロケーション実装
‘ =================================================================
Private m_Data() As Variant
Private m_Capacity As Long ‘ 現在確保している物理メモリ上のサイズ
Private m_Count As Long ‘ 実際に格納された論理データ数
Private Sub Class_Initialize()
Me.Clear
End Sub
‘ 初期化・リセット
Public Sub Clear()
m_Capacity = 0
m_Count = 0
Erase m_Data
End Sub
‘ データの追加(O(1) 償却計算量)
Public Sub Add(ByVal Value As Variant)
‘ 物理容量が不足する場合、容量を倍増させる(最小容量は4)
If m_Capacity <= m_Count Then
If m_Capacity = 0 Then
m_Capacity = 4
Else
m_Capacity = m_Capacity 2
End If
' 領域の拡張(Preserveは容量拡張時のみ発生)
ReDim Preserve m_Data(0 To m_Capacity - 1)
End If
' 値の格納(オブジェクトの場合は適切にSetを考慮する必要があるが、
' ここでは汎用Variantとして値/参照をそのまま受け取る)
If IsObject(Value) Then
Set m_Data(m_Count) = Value
Else
m_Data(m_Count) = Value
End If
m_Count = m_Count + 1
End Sub
' 正確なデータ数(Count)に配列を切り詰めて返す
Public Function ToArray() As Variant
If m_Count = 0 Then
ToArray = Array()
Exit Function
End If
' 返却用に最終的なサイズへ縮小
Dim result() As Variant
ReDim result(0 To m_Count - 1)
Dim i As Long
For i = 0 To m_Count - 1
If IsObject(m_Data(i)) Then
Set result(i) = m_Data(i)
Else
result(i) = m_Data(i)
End If
Next i
ToArray = result
End Function
' プロパティ: 現在の要素数
Public Property Get Count() As Long
Count = m_Count
End Property
---
3. レガシー環境・API連携における注意点
実務において、動的配列を扱うもう一つの大きな山場が、Windows API(特に `SafeArray` やポインタ操作)や、外部COMコンポーネントとの連携である。
VBAの配列は内部構造として `SAFEARRAY` 構造体を保持している。これをAPIに渡す際、以下の鉄則を遵守しなければ、メモリリークや致命的なACCESS_VIOLATION(VBAの突然の強制終了)を引き起こす。
1. 未初期化配列の渡し制御
`Dim arr() As String` と宣言した直後の配列は、まだ `SAFEARRAY` が実体化していない(指针が `NULL`)。この状態の配列をAPIに渡すとクラッシュする。必ず `ReDim arr(0)` などで実体を生成してから渡すこと。
2. 多次元配列のメモリレイアウト
VBAの配列は列優先(Column-major order)でメモリに配置される(C言語系は行優先)。API側がC言語のメモリレイアウトを期待している場合、2次元配列をそのまま渡すとデータの整合性が崩れる。必要に応じて1次元配列へ平坦化(フラット化)してやり取りするのが、レガシーシステム連携の定石である。
—
4. チーフアーキテクトからの提言
VBAは「おもちゃの言語」と揶揄されることがある。しかし、それは言語の仕様を理解せず、場当たり的なコードを量産した結果に過ぎない。
`ReDim Preserve` が持つコストの本質を理解し、メモリの再割り当て回数をコントロール下におくことで、VBAは数十万件のレコード処理をも一瞬で完遂する堅牢なエンタープライズ・エンジンへと変貌する。
動的配列を制する者は、VBAのメモリ管理を制す。
あなたの書くコードに、妥協のないアーキテクチャの美しさと圧倒的なパフォーマンス宿らんことを。
