VB.NET配列の呪縛:`ReDim Preserve`の隠れたコストと、モダンなコレクションへの脱却
VB.NET、そしてその源流であるVisual Basic 6.0(さらにはVBA)の時代から、私たちは「動的配列」という甘い蜜を吸い続けてきた。
要素数が事前に予測できないデータを目の前にした時、エンジニアの手は自然と`ReDim Preserve`へと伸びる。
「とりあえず小さく作っておいて、足りなくなったら拡張すればいい」
この直感的な構文は、プログラミング初学者にとって救いの神であったのと同時に、レガシーシステムのパフォーマンスを内側から蝕むシロモノでもある。
今回は、`ReDim Preserve`がメモリ上で何を引き起こしているのかという低レイヤーの真実を暴き、現代の.NET環境(.NET Frameworkから最新の.NET 8まで)において私たちが取るべき最善の選択肢を提示する。
—
1. `ReDim Preserve`の正体:メモリの「再割り当てとコピー」の地獄
まず、ハードウェアとOSのメモリ管理の視点から、`ReDim Preserve`が実行されたときに何が起きているのかを直視しよう。
配列の本質は、「連続したメモリ空間の確保」である。
メモリ上に `[要素0][要素1][要素2]` と、隙間なく並んでいるからこそ、インデックスによる $O(1)$ の超高速アクセスが担保される。
ここに `ReDim Preserve ary(3)` として要素を追加しようとした時、もし直後のメモリ領域がすでに別のオブジェクト(あるいはOSの別プロセス)によって占有されていたらどうなるか?
VB.NET(CLR)の挙動はこうだ。
1. 新しいサイズ(この場合は4つ分以上)の連続したメモリ領域を、ヒープ上の全く別の場所に新しく確保する。
2. 既存の古いメモリ領域から、新しいメモリ領域へ、全要素を1つずつバイト単位でコピーする。
3. 古いメモリ領域を解放する(ガベージコレクションの対象にする)。
$O(N^2)$ というアルゴリズム的暴力
これをループ内で毎回実行したと想像してほしい。
.net
‘ 【アンチパターン】絶対にやってはならないコード
Dim data() As Integer = {}
For i As Integer = 1 To 100000
ReDim Preserve data(i – 1)
data(i – 1) = i
Next
このコードの計算量は $O(N^2)$ である。
要素数が10万件に達する頃には、CPUはデータ処理ではなく「メモリの確保とコピー」にその大半のパワーを奪われる。
数メガバイト程度のデータであっても、ヒープメモリ(Large Object Heapなど)を細切れにし、LOHフラグメンテーションを引き起こす元凶となる。これが、レガシーシステムが突然重くなる隠れた理由だ。
—
2. 現場の現実:なぜ未だに `ReDim Preserve` が使われるのか
「そんなことは百も承知だ。だが、社内システムの古いコードベースにはこれが溢れている」
そう溜息をつくシニアエンジニアも多いだろう。
VB6やVBAからの移行期に書かれたコード、あるいは「とりあえず動くもの」を急ぎで実装した外部ベンダーの成果物。これらには、サイズが不確定なテキストファイル読み込み、DBからの逐次フェッチ処理、あるいはWin32 APIから返る可変長データの受取バッファとして、未だに `ReDim Preserve` が跋扈している。
レガシー環境の保守において、これを全て書き換えるのはリスクを伴う。しかし、パフォーマンスボトルネックが明らかにここにある場合、メスを入れざるを得ない。
ここで、`ReDim Preserve` を使う場合の「せめてもの延命措置」としての事前サイジング(Over-allocation)のテクニックを見ておこう。
.net
‘ やむを得ず配列を使う場合の最適化(キャパシティ事前確保の概念)
Dim capacity As Integer = 1000
Dim data() As Integer = New Integer(capacity – 1) {}
Integer actualCount = 0
While HasMoreData()
If actualCount >= capacity Then
‘ 容量が足りなくなったら、現在の2倍のサイズで一気に拡張する(コピー回数を激減させる)
capacity = 2
ReDim Preserve data(capacity – 1)
End If
data(actualCount) = GetNextData()
actualCount += 1
End While
‘ 最後に実際のサイズに切り詰める
ReDim Preserve data(actualCount – 1)
この「2倍拡張(Geometric Growth)」アルゴリズムは、後述する `List(Of T)` の内部実装そのものである。自前で配列を管理せざるを得ないレガシーなWin32 API連携などの特殊な場面では、この発想が命を救う。
—
3. 究極の解:`List(Of T)` への完全移行とイディオム
しかし、通常のビジネスロジックやデータ処理において、もはや自前で配列のサイズを管理すべきではない。.NETが提供するジェネリックコレクション `System.Collections.Generic.List(Of T)` こが、この問題に対する唯一にして究極の解である。
なぜ `List(Of T)` なのか?
1. 償却計算量 $O(1)$ の実現: 前述の「2倍拡張戦略」が内部で完全にカプセル化されている。ユーザーはメモリ管理の苦痛から解放される。
2. 型安全(Type Safety): `Object` 型ではなくジェネリックであるため、ボクシング/アンボクシングのオーバーヘッドがなく、プリミティブ型でもパフォーマンスが低下しない。
3. 豊富なLINQサポート: 配列のままでは泥臭く書くしかなかったフィルタリングや集計が、洗練された構文で記述できる。
以下に、実務で即座に使える `List(Of T)` への移行パターンを示す。
.net
Imports System.Collections.Generic
Public Class DataProcessor
‘ 【推奨】動的データには常に List(Of T) を使用する
Public Sub ProcessVariableData()
Dim dataList As New List(Of Integer)()
‘ 10万件のデータ追加も、内部バッファの倍加制御により一瞬で終わる
For i As Integer = 1 To 100000
dataList.Add(i)
Next
‘ 配列としてどうしてもAPIに渡す必要がある場合のみ、最後にToArray()を呼ぶ
‘ これにより、「コピーは最後の一回だけ」に限定される
Dim finalArray() As Integer = dataList.ToArray()
‘ 処理の実行…
ExecuteWin32Api(finalArray)
End Sub
Private Sub ExecuteWin32Api(ByRef data() As Integer)
‘ Win32 API 連携のシミュレーション
‘ 配列のポインタを固定して渡す必要がある場合はここで行う
End Sub
End Class
—
4. チーフアーキテクトからの提言:モダン.NETへの適応
システム開発の現場において、コードの「行数の短さ」や「書いたときの楽さ」に惑わされてはならない。
`ReDim Preserve` は、その手軽さの裏側で、CPUキャッシュのミスヒット、メモリの断片化、GC(ガベージコレクション)への過剰な負荷という、システム全体の致命傷になり得るコストを支払わせている。
VB.NETは、JavaやC#に引けを取らない強力なモダン言語である。
レガシーな作法を引きずることを良しとせず、背後で何が起きているのかを常に想像する「ハードウェアに近い視点」を持つこと。それこそが、真に堅牢でスケーラブルなエンタープライズシステムを構築する唯一の道である。
今日からあなたのコードベースにある `ReDim Preserve` を検出し、適切な `List(Of T)` へリファクタリングせよ。システムは確実に軽快な息吹を取り戻すはずだ。
