1. はじめに:なぜ「ゼロ・アロケーション」なのか?
60FPS(約16.6ms/フレーム)、あるいは120FPSといった高フレームレートが求められるゲームやシミュレーションでは、**ガベージコレクション(GC)による一時停止(スパイク)**が深刻な問題になります。
ループ内で何気なく new Vector3() を生成したり、オブジェクトを頻繁に作成・破棄したりすると、ヒープ領域が徐々に圧迫され、やがてGCが発動します。
この数ミリ秒〜数十ミリ秒の停止が、ユーザーにとって明確な「カクつき(スタッタリング)」として知覚されます。
この問題を根本から回避する設計思想が、
実行中に一切のメモリアロケーションを行わない「ゼロ・アロケーション」
です。
2. データ構造の転換:AoOからSoAへ
JavaScriptやC#のような言語では、オブジェクト(参照型)の生成は基本的にヒープへのアロケーションを伴います。
そのため、以下のような直感的なデータ構造:
-
AoO(Array of Objects)
[{x:1, y:2}, {x:3, y:4}, ...]
は、GC負荷の観点では不利です。
代わりに採用するのが:
-
SoA(Structure of Arrays)
{ x: [1, 3, ...], y: [2, 4, ...] }
この構造により:
- オブジェクト生成が不要になる
- メモリが連続配置される
- キャッシュ効率が向上する
といった利点が得られます。
3. Typed Arrayによるフラットなメモリ管理
SoAをさらに推し進めると、Float32Array や Uint32Array といったTyped Arrayによる管理に行き着きます。
重要なポイントは次の通りです:
- 初期化時に最大サイズを一括確保
- 実行中はインデックスアクセスのみ
- オブジェクト生成・破棄は一切行わない
実装例:ゼロ・アロケーションなエンティティ管理
class ZeroAllocEntityManager {
constructor(maxEntities) {
this.maxEntities = maxEntities;
this.activeCount = 0;
// 初期化時にのみメモリ確保
this.positionsX = new Float32Array(maxEntities);
this.positionsY = new Float32Array(maxEntities);
this.velocitiesX = new Float32Array(maxEntities);
this.velocitiesY = new Float32Array(maxEntities);
this.isActive = new Uint8Array(maxEntities);
}
spawn(x, y, vx, vy) {
if (this.activeCount >= this.maxEntities) return -1;
const i = this.activeCount;
this.positionsX[i] = x;
this.positionsY[i] = y;
this.velocitiesX[i] = vx;
this.velocitiesY[i] = vy;
this.isActive[i] = 1;
this.activeCount++;
return i;
}
update() {
for (let i = 0; i < this.activeCount; i++) {
if (this.isActive[i]) {
this.positionsX[i] += this.velocitiesX[i];
this.positionsY[i] += this.velocitiesY[i];
}
}
}
}
この実装では:
-
newはコンストラクタ時のみ - フレーム中のアロケーションはゼロ
- GCは実質発生しない
という状態を実現できます。
4. GPU転送との高い親和性
ゼロ・アロケーション設計の大きな利点の一つが、GPUとのデータ連携の効率化です。
AoO(オブジェクト配列)の場合:
- GPU用にデータを再パック(シリアライズ)する必要がある
- その過程で追加のアロケーションが発生する
一方、Typed Arrayで管理している場合:
- メモリがすでにフラットなバイナリ形式
- そのままGPUバッファへ転送可能(例:
writeBuffer)
これにより:
- CPU負荷の削減
- GC発生の抑制
- 転送コストの最小化
を同時に実現できます。
さらに、連続メモリ配置によってCPUキャッシュ効率(データ局所性)も向上し、純粋な演算性能も改善されます。
5. 運用上の注意点
ゼロ・アロケーションは強力ですが、トレードオフもあります。
- コードの抽象度が下がる(低レベル寄りになる)
- 可読性・保守性が低下しやすい
- 最大数(プールサイズ)を事前に決める必要がある
そのため:
- パフォーマンスクリティカルな箇所に限定して適用する
- 上位レイヤーでは通常のオブジェクト設計を維持する
といったバランスが重要です。
6. まとめ
ゼロ・アロケーションは、
「オブジェクトではなく、データとして扱う」
という発想の転換に基づく最適化手法です。
- GCスパイクの排除
- CPUキャッシュ効率の向上
- GPU転送の高速化
といった複数のメリットを同時に得られる一方で、設計にはある程度の割り切りが求められます。
数万〜数十万規模のデータをリアルタイムで扱う場合、このアプローチは非常に有効です。
パフォーマンスの限界を押し上げたい場面で、ぜひ選択肢に入れてみてください。