GCの悪魔を退治せよ:VB.NETにおけるSpan
VBAで `String` 型の連結を繰り返してメモリを枯渇させたり、`SubString` を乱用してヒープをGC(ガベージコレクション)のゴミ捨て場に変えてしまった経験はないだろうか。
現代の.NET環境においても、VB.NETの書きやすさは強力な武器だが、同時に「メモリを浪費しやすい」という側面も併せ持っている。特に高頻度で呼ばれるデータパーサや、巨大なバイナリ・文字列を扱うシステムにおいて、安易な `SubString` や配列のコピーは、パフォーマンスを殺す毒薬だ。
今回は、VB.NETで「ゼロアロケーション」を実現し、GCの介入を極限まで減らすための武器、`Span
—
1. なぜ「コピー」がシステムを殺すのか
VB.NETで `MyString.Substring(0, 10)` を実行したとき、舞台裏では何が起きているか。
1. 新しい `String` オブジェクトがヒープ上に確保される。
2. 元の文字列から指定範囲の文字がコピーされる。
3. 元の文字列はそのまま残り、新しい文字列が生成される。
これをループ内で数万回繰り返せば、GCは悲鳴を上げ、アプリケーションは「プチフリーズ」を繰り返す。特にWindows APIとの連携や高頻度な通信処理では、この微小なアロケーションが命取りになる。
Span
—
2. Span を駆使した高速パーシングの極意
以下のコードは、巨大なログファイルやバイナリデータを、アロケーションなしで切り出す手法だ。
Imports System
Imports System.Runtime.InteropServices
Public Module MemoryOptimizer
”’
”’
Public Sub ProcessData(input As String)
‘ StringをReadOnlySpan(Of Char)に変換(コピーは発生しない)
Dim span As ReadOnlySpan(Of Char) = input.AsSpan()
‘ スペースで分割するような処理をシミュレート
Dim firstPart As ReadOnlySpan(Of Char) = span.Slice(0, 5)
Dim secondPart As ReadOnlySpan(Of Char) = span.Slice(6, 4)
‘ 注意: Spanはヒープに置けないため、クラスのフィールドにはできない。
‘ あくまでメソッド内の局所的な高速処理に徹する。
Console.WriteLine($”Part1: {firstPart.ToString()}, Part2: {secondPart.ToString()}”)
End Sub
End Module
なぜこれが最強なのか
- スタックアロケーション: `Span
` は `ref struct` であり、スタック領域で管理される。GCの追跡対象外だ。 - Sliceのコスト: `Slice` メソッドはポインタ演算に近い操作を行うだけで、メモリ確保を一切行わない。
—
3. Memory:非同期処理との共生
`Span
`Memory
—
4. 現場で生き残るための「鉄則」
レガシーなシステムを保守・改修する際、この技術を導入する際は以下の点に注意せよ。
1. 境界チェックのコスト: `Span
2. Windows API連携: `MemoryMarshal.GetReference()` を使えば、`Span` の先頭アドレスを直接取得し、アンマネージドなAPIに直接ポインタを渡すことが可能だ。`Marshal.AllocHGlobal` でメモリを確保し、`Span` でラップする手法は、大規模なバイナリ処理の定石である。
3. VB.NETの制約: VB.NETはC#に比べて新機能の恩恵が遅れる傾向があるが、`System.Memory` NuGetパッケージを導入すれば、.NET Framework 4.6.1以降でも `Span
—
チーフアーキテクトからの提言
「モダンな記述=遅い」という誤解を捨てろ。`Span
システムが遅いのは、コードが古いからではない。「メモリを無駄にコピーし続けているから」だ。GCを走らせるな。CPUサイクルをゴミの掃除に使わせるな。
今日のコードから `String.Substring` を `AsSpan().Slice` に置き換えてみるだけでいい。その小さな一歩が、数百万レコードを処理するシステムの命運を分けることになる。
実装で迷ったら、メモリレイアウトを想像せよ。それが、真のエンジニアへの道だ。
