【テクニカル・上級編】【動的配列の極意】ReDim Preserve のオーバーヘッドを最小化する配列自動拡張ロジックとSafeArray操作 – VBScript (Visual Basic Scripting Edition)解析バイブル

スポンサーリンク

【動的配列の極意】ReDim Preserve のオーバーヘッドを最小化する配列自動拡張ロジックとSafeArray操作

レガシーシステムの深部、あるいは現代のWindows自動化スクリプトの裏側において、VBScript(Visual Basic Scripting Edition)は今なおしぶとく生き続けている。WSH(Windows Script Host)環境における軽量性、COMコンポーネントへのシームレスな接続性。これらは他のモダン言語にはない独自の強みだ。

しかし、VBScriptを真に「掌握」しているエンジニアはどれほどいるだろうか。
特に、データ構造の基本である「配列」の扱いにおいて、多くの開発者は致命的な設計ミスを犯している。その代表例が、ループ内での安易な `ReDim Preserve` の多用だ。

今回は、VBScriptの動向の裏でうごめくSafeArrayのメモリ管理メカニズムを解剖し、O(N^2)の地獄からスクリプトを救い出す「倍々バッファリング戦略(Geometric Growth)」と、実用的な自動拡張配列クラスの実装コードを提示する。

—

1. なぜ `ReDim Preserve` は「悪魔の構文」なのか?

VBScriptの動的配列は、内部的にはCOMの基盤技術である SafeArray 構造体としてメモリ上に存在している。
`ReDim Preserve arr(newIndex)` を実行した瞬間、VBScriptの裏側(OLEAUT32.dll)では以下の重厚な処理が走る。

1. 新規メモリの確保: `newIndex` のサイズに見合う新しい連続したメモリブロックをヒープ上に確保する。
2. データのコピー: 既存のメモリブロックから新しいメモリブロックへ、全要素のバイナリコピーを行う(これが最悪のオーバーヘッド)。
3. 旧メモリの解放: 以前のメモリ領域を破棄する。

これを要素が追加されるたび(毎ループ)に実行するとどうなるか?
要素数 $N$ に対し、コピー回数は $1 + 2 + 3 + \dots + N = O(N^2)$ となり、データ量が数万件を超えた瞬間にスクリプトは完全にフリーズする。これが「`ReDim Preserve` の呪い」だ。

—

2. オーバーヘッドを最小化する「倍々バッファリング」戦略

このパフォーマンス劣化を防ぐ唯一にして最大の定石が、コンピュータ科学における「Geometric Growth(幾何学的成長)」、すなわち倍々バッファリングである。

要素を1つ追加するたびに配列を広げるのではなく、あらかじめ大きめの物理領域(バッファ)を確保しておき、容量が溢れた時だけ、サイズを2倍(あるいは1.5倍)に拡張して再割り当てを行う。

このアルゴリズムを適用した場合、メモリの再割り当てとコピーが発生する頻度は $O(\log N)$ に激減し、償却計算量(Amortized Time Complexity)は $O(1)$ となる。

—

3. 実装:VBScriptにおける「スマート可変長配列クラス」

VBScriptにはJavaの `ArrayList` のような洗練された動的コレクションがない(`Scripting.Dictionary` はキー・バリュー型であり、順序保証やシーケンシャルな数値インデックス処理にはオーバースペックかつ遅い)。

そこで、クラス構文(`Class`)を用いて、内部バッファを隠蔽した高効率な動的配列ラッパーを実装する。

‘ ==============================================================================
‘ 致命的なReDim地獄を回避する、高効率動的配列クラス (SmartArray)
‘ ==============================================================================
Class SmartArray
Private m_arr() ‘ 内部物理バッファ
Private m_capacity ‘ 物理的に確保されている最大サイズ
Private m_count ‘ 実際に格納されている要素数

‘ コンストラクタ初期化
Private Sub Class_Initialize()
m_capacity = 4 ‘ 初期バッファサイズ(小さすぎず大きすぎず)
m_count = 0
ReDim m_arr(m_capacity – 1)
End Sub

‘ 要素の追加 (O(1) 償却計算量)
Public Sub Add(ByVal value)
‘ 容量が限界に達したら倍に拡張
If m_count >= m_capacity Then
m_capacity = m_capacity 2
ReDim Preserve m_arr(m_capacity – 1)
End If

‘ データの格納とカウンタインクリメント
SetIfObject m_arr(m_count), value
m_count = m_count + 1
End Sub

‘ 指定インデックスの要素取得
Public Property Get Item(ByVal index)
If index < 0 Or index >= m_count Then
Err.Raise 9, “SmartArray”, “インデックスが範囲外です: ” & index
End If
If IsObject(m_arr(index)) Then
Set Item = m_arr(index)
Else
Item = m_arr(index)
End If
End Property

‘ 現在の要素数を返す
Public Property Get Count()
Count = m_count
End Property

‘ 最終的なトリミング済み配列を返す(出力用)
Public Function ToArray()
If m_count = 0 Then
ToArray = Array()
Exit Function
End If

‘ 使用している領域ぴったりに縮小
Dim temp()
temp = m_arr
ReDim Preserve temp(m_count – 1)
ToArray = temp
End Function

‘ 内部ヘルパー: オブジェクト代入と値代入の安全なスイッチング
Private Sub SetIfObject(ByRef target, ByVal source)
If IsObject(source) then
Set target = source
Else
target = source
End If
End Sub
End Class

この実装のアーキテクチャ的ポイント

  • カプセル化: 呼び出し側は `ReDim` や `m_capacity` の存在を意識する必要がない。
  • COM/オブジェクトの安全な扱い: VBScript特有の `Set` と値代入の差異を吸収するため、`SetIfObject` ヘルパーを挟むことで、COMオブジェクト(ADO RecordsetやFileSystemObjectなど)の格納破綻を防いでいる。
  • 終端トリミング (`ToArray`): 処理完了後は、無駄に確保された余分なバッファを切り捨て、純粋なVBScriptネイティブ配列として呼び出し元へ返却する。

—

4. 実戦投入:ベンチマークとメモリ最適化の極意

この `SmartArray` を使って、10万件の文字列結合・データを処理するスクリプトを組んでみよう。

‘ — テスト実行スクリプト —
Option Explicit

Dim sw: sw = Timer()
Dim sa: Set sa = New SmartArray

Dim i
For i = 1 To 100000
sa.Add “Data_Record_” & i
Next

WScript.Echo “要素数: ” & sa.Count & ” / 処理時間: ” & (Timer() – sw) & ” 秒”

‘ 最終配列の取得
Dim finalArr: finalArr = sa.ToArray()

‘ ※巨大なオブジェクトを扱う場合は、明示的な参照解放が必須
Set sa = Nothing

シニアエンジニアが知るべき「メモリの呪縛」

VBScriptはガベージコレクション(GC)を持つが、その実態は参照カウント方式(Reference Counting)と循環参照ウォッチャーの組み合わせである。
特に、WSHスクリプトが長期間稼働するバッチ処理や、巨大なADO Recordsetを配列に展開するようなシーンでは、スコープを抜けるまでメモリが解放されないケースが多々ある。

1. 変数の明示的破棄: 大規模な配列やCOMオブジェクトを格納した変数は、使い終わったら即座に `Set obj = Nothing` または `Erase arr` を実行せよ。
2. スコープの細分化: 処理の塊ごとに `Sub` や `Function` に閉じ込め、ローカル変数のライフサイクルを強制的に短縮させること。これにより、SafeArrayが占有していたヒープメモリが即座にOSへ返還される確率が高まる。

—

5. 総括

VBScriptは「古い言語」ではない。Windows環境において、インストーラなしで即座に動作し、OSの深部にまでアクセスできる極めてスパルタンで強力な「CUIランタイム」である。

`ReDim Preserve` の挙動を理解せず、その場しのぎのコードを書くことは、時限爆弾を抱えたスクリプトを量産することに等しい。
SafeArrayの構造とメモリ割り当てのコストを正しく把握し、今回提示したような「バッファリング思想」を組み込むことで、レガシー環境であってもモダン言語に匹敵する堅牢性とパフォーマンスを引き出すことが可能となる。

技術の優劣を語る前に、目の前のコンポーネントの仕様を極限までハックせよ。それこそが、真のインフラストラクチャ・エンジニアの流儀である。

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