VB.NETで「GCの悪夢」を終わらせる:SpanとMemoryによる極限のゼロアロケーション
業務自動化の現場で、数百万件のログ解析や巨大なCSVパーサを実装した際、「最初は速かったのに、データ量が増えると急激に重くなる」という現象に直面したことはないだろうか。その正体は、CPUの計算能力不足ではない。ガベージコレクション(GC)による停止時間(Stop-the-world)だ。
VB.NETにおいて、文字列の切り出し(Substring)や配列のコピー(ToArray)を繰り返すことは、ヒープメモリに無駄なゴミを撒き散らす行為に等しい。本稿では、`.NET Core / .NET 5+`で導入された`Span
—
なぜ従来のVB.NETコードは「重い」のか
従来のVB.NETで文字列を分割する際、多くのエンジニアは以下のようなコードを書く。
‘ アンチパターン:これでは毎回新しい文字列インスタンスがヒープに生成される
Dim parts As String() = longString.Split(“,”c)
このコードを実行するたびに、元の文字列とは別に新しい`String`オブジェクトがメモリ上に生成される。数百万回のループでこれを行えば、GCが頻繁に作動し、アプリケーションの応答性は崩壊する。
我々が目指すべきは「既存のメモリ領域を分割し、参照する」こと。 それを可能にするのが`Span
—
Span:メモリの「窓」を切り出す魔法
`Span
実践:ゼロアロケーション・パーサの実装例
ファイル読み込み処理において、文字列の全コピーを行わずに特定のデータを抽出するコード例を示す。
Imports System
Imports System.Runtime.InteropServices
Public Module ParserEngine
”’
”’
Public Sub ProcessData(ByVal rawData As String)
‘ 文字列を直接Spanとして扱う(ReadOnlySpan
Dim span As ReadOnlySpan(Of Char) = rawData.AsSpan()
‘ カンマ区切りの位置を探す(メモリのコピーは発生しない)
Dim commaIndex As Integer = span.IndexOf(“,”c)
If commaIndex <> -1 Then
‘ Sliceメソッドはメモリをコピーせず、範囲を指定した「窓」を作るだけ
Dim field1 As ReadOnlySpan(Of Char) = span.Slice(0, commaIndex)
‘ 比較もアロケーションなしで行う
If field1.SequenceEqual(“TARGET_ID”c) Then
Console.WriteLine(“IDが一致しました”)
End If
End If
End Sub
End Module
なぜこれが「堅牢」なのか
1. GC負荷の激減: 新しいオブジェクトを生成しないため、GCのトリガーを引くことがない。
2. 境界チェックの最適化: `Span
3. 汎用性: `Byte`配列、`Char`配列、スタック上のメモリなど、場所を問わず同じロジックで操作できる。
—
実務における注意点:Spanの制約
`Span
- クラスのフィールドにはできない: スタック上にのみ存在できるため、クラスのメンバ変数にはできない。
- 非同期処理(Async/Await)では扱えない: 非同期メソッドの境界を越えて保持できないため、`Memory
`を併用する必要がある。
Memoryの使い分け
`Span
‘ 非同期処理で使う場合はMemory(Of T)へ変換する
Dim data As Byte() = GetLargeData()
Dim memory As Memory(Of Byte) = data.AsMemory(0, 100)
Await Stream.WriteAsync(memory)
—
プロフェッショナルへの提言
業務自動化ツールを開発する際、「とりあえず動く」コードを書くのは初級者だ。中級者は「メモリ消費量を気にする」。そして一流のエンジニアは「GCのサイクルを制御する」。
今回紹介した手法を導入すれば、これまで「重すぎて処理できなかった」数GB単位のログファイルや、数万件のDBレコード一括処理が、驚くほど軽快に動作するようになるはずだ。
コードを書く際は常に自問してほしい。「この変数生成は、本当に必要か?」と。メモリの配置を意識する設計こそが、枯れた技術であるVB.NETを現代の超高速実行エンジンへと変貌させる鍵となる。
さあ、あなたのコードから不要な「ゴミ」を削ぎ落とし、極限のパフォーマンスを体感してほしい。
