【テクニカル・上級編】VBAで「動的配列」を操る:RedimとPreserveでデータ量を柔軟に扱う – Excel VBA解析バイブル

スポンサーリンク

動的配列の真髄: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のメモリ管理を制す。
あなたの書くコードに、妥協のないアーキテクチャの美しさと圧倒的なパフォーマンス宿らんことを。

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