【テクニカル・上級編】【低メモリ型重複排除アルゴリズム】Dictionaryを使用せずに純粋な配列操作で大容量データを高速ユニーク化する手法 – VBScript (Visual Basic Scripting Edition)解析バイブル

スポンサーリンク

【低メモリ型重複排除アルゴリズム】Dictionaryを使用せず純粋な配列操作で大容量データを高速ユニーク化する手法

レガシーなWindowsインフラストラクチャや、厳格にサンドボックス化されたエンタープライズ環境において、VBScript(WSH)はいまだにキメラのようなシステム間連携のグルー言語として静かに、しかし強烈に稼働し続けている。

日々の業務自動化において、数万行から数百万行に及ぶテキストログやCSVストリームを処理する際、最大のボトルネックとなるのが「重複排除(ユニーク化)」の処理だ。

一般的なVBScriptの解説書を開けば、決まり文句のように `Scripting.Dictionary` オブジェクトの `Exists` メソッドと `Add` メソッドを使ったハッシュベースの重複排除が紹介される。しかし、シニアエンジニアであれば知っているはずだ。数百万件規模のデータに対して `Scripting.Dictionary` を生成・操作すると、COMラッパーのオーバーヘッドによる激しいメモリ肥大化(Bloat)を引き起こし、最悪の場合はVBScriptのプロセス空間(WScript.exe / CScript.exe)がメモリ不足(Out of Memory)でクラッシュすることを。

今回は、COMオブジェクトの生成コストを完全に排除し、純粋な配列操作と二分探索(Binary Search)、そしてインメモリでの高速ソートを駆使して、極限までメモリを削ぎ落とした「低メモリ型重複排除アルゴリズム」の真髄を解説する。

1. なぜ `Scripting.Dictionary` は大容量データで破綻するのか?

`Scripting.Dictionary` は内部でハッシュテーブルを構築するため、キーの検索・追加が $O(1)$ のオーダーで行えるという利便性がある。しかし、VBScriptのバックグラウンドで動作するこのCOMコンポーネントには、以下の致命的な構造的欠陥がある。

1. COM境界を跨ぐコスト: スクリプトエンジンとDLL間のマーシャリングが発生し、数百万回の往復で実行サイクルがスポイルされる。
2. ポインタ配列と文字列バッファの肥大化: Dictionary自体が保持するメタデータ、各エントリのVariant型の構造体、ハッシュ衝突を回避するためのチェイン構造により、格納する実データの何倍ものメモリフットプリントを消費する。

この限界を突破するためには、「データをあらかじめソートし、連続する重複要素をインプレース(あるいは最小限のバッファコピー)で削ぎ落とす」という、C言語やアセンブリの時代から脈々と続く低レイヤーのアルゴリズムをVBScript上で模倣する必要がある。

2. アーキテクチャの全体像

今回構築する高速ユニーク化エンジンは、以下の3ステップで構成される。

1. 動的配列への一括取り込み: ファイルI/Oの回数を極限まで減らし、メモリ上の1次元配列へ全データを突っ込む。
2. インメモリ・クイックソート: 配列を昇順に並び替え、同一の値を物理的に隣接させる。
3. 二分探索(オプション)または走査型圧縮: 隣接要素比較による $O(N)$ の線形走査、あるいは高速なインプレース圧縮により、重複を排除したコンパクトな配列を再構築する。

ここでは、VBScriptの限界である「遅さ」を補うため、VBScriptのネイティブ機能(あるいはWindows Script Components等に頼らない)で完結する洗練されたソート&圧縮アルゴリズムの実装コードを提示する。

3. 実装コード:極限最適化されたユニーク化スクリプト

以下のコードは、数万〜数百万件の文字列データを `Scripting.Dictionary` を一切使わずに高速かつ低メモリで重複排除するための実践的なVBScriptモジュールである。

Option Explicit

‘ =========================================================================
‘ módulo: LowMemoryUniqueEngine.vbs
‘ 概要: Scripting.Dictionaryを使用せず、配列のソートと隣接走査によって
‘ 大容量データの重複排除を高パフォーマンスかつ低メモリで実行する。
‘ =========================================================================

Sub Main()
Dim objFSO, objFile
Dim colData()
Dim lngCount, i
Dim dtmStart

WScript.Echo “=== 低メモリ型重複排除エンジン起動 ===”
dtmStart = Timer

‘ 1. ダミーの大容量データ生成(テスト用: 10万件、重複多数)
lngCount = 100000
ReDim colData(lngCount – 1)

Randomize
For i = 0 to lngCount – 1
‘ 0から9999までのランダムな数値を文字列化して格納(高確率で重複が発生)
colData(i) = “ID_” & FormatNumber(Int((10000 – 0 + 1) Rnd + 0), 0, 0, 0, 0)
Next

WScript.Echo “データ生成完了. 処理前要素数: ” & UBound(colData) + 1 & ” (経過: ” & FormatNumber(Timer – dtmStart, 2) & “秒)”

‘ 2. 高速クイックソートの実行
dtmStart = Timer
QuickSort colData, LBound(colData), UBound(colData)
WScript.Echo “ソート完了. (経過: ” & FormatNumber(Timer – dtmStart, 2) & “秒)”

‘ 3. 隣接走査による重複排除(インプレース圧縮)
dtmStart = Timer
Dim lngUniqueCount
lngUniqueCount = RemoveDuplicates(colData)
WScript.Echo “重複排除完了. ユニーク要素数: ” & lngUniqueCount & ” (経過: ” & FormatNumber(Timer – dtmStart, 2) & “秒)”

‘ メモリ解放の明示化(VBScriptのガベージコレクタに依存しない強固なライフサイクル管理)
Erase colData
WScript.Echo “=== 処理終了 ===”
End Sub

‘ =========================================================================
‘ クイックソート実装 (HoareのパーティションスキームのVBScript適応版)
‘ =========================================================================
Sub QuickSort(arr, low, high)
Dim first, last, mid, pivot, temp

first = low
last = high

‘ 要素数が極小の場合は直接法を適用するかリターン
If low >= high Then Exit Sub

‘ ピボットの選定(中央値をとることで最悪計算量を回避)
mid = arr((low + high) \ 2)

Do While first <= last Do While arr(first) < mid And first < high first = first + 1 Loop Do While arr(last) > mid And last > low
last = last – 1
Loop

If first <= last Then ' スワップ temp = arr(first) arr(first) = arr(last) arr(last) = temp first = first + 1 last = last - 1 End If Loop If low < last Then QuickSort arr, low, last If first < high Then QuickSort arr, first, high End Sub ' ========================================================================= ' 隣接走査による重複排除アルゴリズム ' ソート済み配列を受け取り、重複を詰めて有効な要素数(上限インデックス+1)を返す ' ========================================================================= Function RemoveDuplicates(ByRef arr) Dim i, j Dim uBoundVal uBoundVal = UBound(arr) If uBoundVal < 0 Then RemoveDuplicates = 0 Exit Function End If j = 0 For i = 1 To uBoundVal If arr(i) <> arr(j) Then
j = j + 1
arr(j) = arr(i)
End If
Next

‘ 配列のサイズをユニーク数に切り詰める (Redim Preserve)
ReDim Preserve arr(j)

RemoveDuplicates = j + 1
End Function

‘ エントリポイントの呼び出し
Main

4. チーフアーキテクトの知見:パフォーマンス最適化の極意

上記のコードを見て、「VBScriptで再帰呼び出し(QuickSort)を使うとスタックオーバーフローのリスクがあるのではないか?」と直感した読者は非常に鋭い。その通りである。通常の素朴なクイックソートは、最悪の場合に $O(N^2)$ の比較回数と深さ $N$ の再帰呼び出しを生み、VBScriptの貧弱なコールスタックを容易に食い潰す。

このリスクを回避し、エンタープライズ環境の堅牢性を担保するためには、以下のチューニングを施す必要がある。

① 再帰深度の抑制と非再帰化(またはスレッドスタック管理)

データ量が数百万件を超えることが確実な場合、VBScriptの再帰関数によるクイックソートではなく、スタック構造体を自前で模倣した非再帰版クイックソート(Iterative QuickSort)へリファクタリングすべきである。これにより、COMおよびスクリプトエンジンのスタック領域の枯渇を完全に防ぐことができる。

② メモリの明示的破棄(`Erase` ステートメントの強制)

VBScriptはスクリプト終了時にメモリを解放するが、長時間稼働するWSHプロセス(タスクスケジューラから常時キックされるバッチ等)において、巨大な配列やオブジェクトがスコープ内に残り続けることはメモリリークの温床となる。
使い終わった動的配列は、必ず `Erase arr` を明示的に実行し、OSへ物理メモリを即座に返還させることがプロフェッショナルの条件である。

③ 文字列比較のロケール依存性に関する注意

VBScriptの比較演算子(`<`, `>`, `=`)は、デフォルトでシステムのロケール(大文字小文字を区別しないなど)に依存する場合がある。厳密なバイナリ一致(Aとaを別のものとして扱うなど)が必要な場合は、`StrComp(string1, string2, vbBinaryCompare)` を組み込むことで、意図しない衝突を防ぎ、パフォーマンスをさらに最適化できる。

総括

「古い技術だから仕方ない」「Dictionaryが使えないから遅くても諦める」――そのような妥協は、プロフェッショナルな業務自動化エンジニアの辞書には存在しない。

言語の仕様の隙間を突き、アルゴリズムの力でハードウェアの制約をねじ伏せる。この純粋な配列操作とソートを組み合わせた低メモリ型重複排除手法は、VBScriptというレガシーの枠組みを超え、あらゆるリソース制限環境における「エンジニアリングの美学」を体現するものである。現場の極限環境において、ぜひこの知見を武器として活用してほしい。

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