【動的配列の極意】ReDim Preserve のオーバーヘッドを最小化する配列自動拡張ロジックとSafeArray操作
開発現場でVBScriptを使ったファイル一括処理やデータベースからのレコードセット取得を行っていて、次のような絶望を味わったことはないだろうか。
「最初はサクサク動いていたのに、処理対象のデータが1万件、10万件と増えるにつれて、終盤に差し掛かった途端にCPUが張り付き、極端に処理が重くなる。最終的にはスクリプトが応答しなくなる――」
犯人は、あなたも日常的に使っている `ReDim Preserve` だ。
今回は、VBScript(WSH)の基盤であるCOMの配列構造(SafeArray)の裏側を暴き、このパフォーマンス殺しのオーバーヘッドを完全に無効化する「倍々バッファリング戦略」による配列自動拡張ロジックを伝授する。
—
なぜ `ReDim Preserve` は「遅い」のか?
VBScriptの動向配列の裏側では、Windowsの底层構造である SafeArray というメモリ管理機構が動いている。
普通のプログラミング言語の動的配列(C++の `std::vector` や Javaの `ArrayList` など)は、あらかじめ余分なメモリ(バッファ)を確保しておき、要素が溢れそうになった時だけ一気にメモリを再確保する。これにより、メモリ確保の回数を劇的に減らしている。
しかし、VBScriptの `ReDim Preserve` は、「要素を1つ増やす(または減らす)たびに、メモリ上の新しい領域にすべての要素を丸ごとコピーし、古い領域を破棄する」 という暴挙を毎回行っている。
計算量(O記法)の罠
- ループの回数を $N$ とする。
- 毎回の `ReDim Preserve` で要素を1つずつ増やすと、コピーされるデータの総量は $1 + 2 + 3 + \dots + N = \frac{N(N+1)}{2}$ となる。
- すなわち、時間計算量は $O(N^2)$。データが10倍になれば、処理時間は100倍に跳ね上がる。これが「後半急激に重くなる」の正体だ。
この呪縛から逃れる唯一の方法が、「メモリの確保回数を劇的に減らす:倍々拡張アルゴリズム(Amortized Analysis)」 である。
—
限界突破:倍々バッファリング(Exponential Growth)の設計思想
あらかじめ必要となりそうなサイズを見積もるか、容量が限界に達した瞬間に「現在のサイズの2倍のメモリを確保し直す」手法を採用する。
これにより、メモリの再割り当て回数は $O(\log N)$ に激減し、償却計算量(Amortized Time Complexity)は $O(1)$ となる。数万件のデータを扱う業務スクリプトにおいて、これは秒単位と分単位の差を生む。
—
プロダクションコード:高速・安全な動動的配列ラッパークラス(に近い構造)
VBScriptにはクラス(Class)構文があるが、純粋な配列操作の高速性を維持するため、今回はそのまま実務のバッチ処理に組み込める「堅牢な配列自動拡張モジュール(関数群)」として提供する。
以下のコードは、エラーハンドリング、初期化、メモリ倍加、そして最終的なトリミング(余分な空白要素の削除)を完璧に網羅したプロダクションコードである。
‘ ==============================================================================
‘ プロジェクト名: HighPerformanceArray.vbs
‘ 概要: ReDim Preserveのオーバーヘッドを排除した高速配列自動拡張エンジン
‘ 備考: 業務システムのログ解析、DBバルク処理の前処理に最適
‘ ==============================================================================
Option Explicit
‘ メイン処理のシミュレーション(10万件のデータを高速に蓄積する例)
Sub Main()
Dim sw: sw = Timer
‘ 1. 配列管理用変数の初期化
‘ 内部バッファ配列、現在の有効要素数、確保済みバッファサイズ
Dim arrBuffer()
Dim intCount : intCount = 0
Dim intCapacity : intCapacity = 0
‘ 初期バッファとして16要素を確保(小さすぎず大きすぎないサイズ)
Call InitializeBuffer(arrBuffer, intCapacity, 16)
‘ 2. 模擬データの追加(例:100,000件)
Dim i
For i = 1 To 100000
‘ データを追加(必要に応じて自動で容量が2倍に拡張される)
Call AppendItem(arrBuffer, intCount, intCapacity, “Data_Record_” & i)
Next
‘ 3. 実際のデータ数に合わせてサイズを切り詰める(Trim)
Call FinalizeBuffer(arrBuffer, intCount)
WScript.Echo “処理完了. 有効要素数: ” & intCount & ” / 処理時間: ” & (Timer – sw) & ” 秒”
‘ (ここにファイル出力やDB一括インサート処理が続く)
End Sub
‘ ——————————————————————————
‘ 初期バッファ確保ルーチン
‘ ——————————————————————————
Sub InitializeBuffer(ByRef buf(), ByRef capacity, ByVal initialSize)
capacity = initialSize
ReDim buf(capacity – 1)
End Sub
‘ ——————————————————————————
‘ 要素追加ルーチン(倍々拡張ロジックの中核)
‘ ——————————————————————————
Sub AppendItem(ByRef buf(), ByRef count, ByRef capacity, ByVal item)
‘ バッファが満杯の場合、容量を2倍に拡張 (Geometric Growth)
If count >= capacity Then
capacity = capacity 2
ReDim Preserve buf(capacity – 1)
End If
‘ データを格納し、カウントをインクリメント
buf(count) = item
count = count + 1
End Sub
‘ ——————————————————————————
‘ 終端処理ルーチン(余分なメモリ領域の切り捨て)
‘ ——————————————————————————
Sub FinalizeBuffer(ByRef buf(), ByVal count)
If count > 0 Then
‘ 実際のデータ数ピッタリにリサイズ
ReDim Preserve buf(count – 1)
Else
‘ データが一件もない場合は空の配列にする
Erase buf
End If
End Sub
‘ 実行
Main()
—
多次元配列(2次元テーブル)を扱う場合の極意
実務では、CSVの読み込みやデータベースのレコードセット(テーブル形式)を扱うため、多次元配列を拡張したい場面に直面する。
ここで重要な仕様上の制約がある。
「VBScriptの `ReDim Preserve` は、多次元配列において『一番右の次元(列側)』しか拡張できない」
言い換えれば、行方向(レコード数)を動的に増やし、列方向(項目数)は固定という構造にする必要がある。
正しい2次元配列の持ち方
- `Data(列数, 行数)` ではなく、`Data(行数, 列数)` として定義し、行方向(第1次元ではなく第2次元)を可変にする……と言いたいところだが、VBScriptの仕様上 `ReDim Preserve` で拡張できるのは一番最後の次元(一番右)である。
- したがって、常に `Data(列固定, 行可変)` の形で定義するか、あるいは「1次元配列の中に、各行の1次元配列(またはDictionary)を格納するジャグ配列(配列の配列)」構造を採用するのが、実務における最も安全でスマートな設計解となる。
‘ 2次元データを扱う場合の推奨アプローチ(1次元配列の入れ子:ジャグ配列)
‘ 各行を独立した1次元配列としてバッファに格納していくことで、
‘ 多次元配列のReDimの制約を完全に回避しつつ、高速な拡張を実現できる。
—
現場でバグらせないための実装上の鉄則
1. 初期バッファサイズは「0」や「1」にしない
最初から `ReDim arr(0)` として1つずつ増やす愚行は避ける。少なくとも `16` や `32`、あるいはファイル行数の見積もりがつくならその1割程度を最初に確保せよ。
2. エラーハンドリング(On Error)の罠に注意
大規模なメモリ再確保時にメモリ不足(Out of Memory)エラーが発生する可能性がある。数百万件クラスのデータを扱う場合は、分割処理(チャンク処理)を検討すべきであり、VBScriptの32bitプロセスの限界(通常2GB、実質1.2〜1.5GB程度)を常に意識すること。
3. 不要になった配列は `Erase` で明示的に解放
VBScriptのガベージコレクションは即時ではない。巨大なバッファ配列を使い回す、あるいは処理が終わったら速やかに `Erase` を呼び出し、COMコンポーネントとしてのメモリフットプリントを最小限に抑えよ。
—
アーキテクトからの総括
「たかがVBScript」と侮って毎回のループで `ReDim Preserve` を書くプログラマは、業務自動化の現場において「遅延爆弾」を仕掛けているのと同じだ。
今回紹介した「倍々バッファリング」は、コンピュータサイエンスの基本にして、レガシー環境を極限までチューニングするための必須教養である。このロジックをあなたのツールキットに組み込むことで、VBScriptは「遅いスクリプト言語」から「頼れる高速バッチエンジン」へと生まれ変わる。
現場の生産性を極限まで引き上げるコードを、ぜひ次のプロジェクトから導入してほしい。
