ブラウザで大量のオブジェクトを動かそうとして、動作がカクついたりメモリ不足に悩まされたことはありませんか?
今回は、宇宙規模のシミュレーションや超高密度な演算を行うシステムで使われる、メモリの極限利用術**「ビットパッキング」**について、初心者の方にもわかりやすく解説します。
1. 「10万個の壁」を突破する考え方
JavaScriptでキャラクターやパーティクルを作る際、通常は以下のようにオブジェクトを生成します。
const entity = {
id: 12345,
status: "ACTIVE",
energy: 85,
isWaiting: false
};
これ、実はブラウザのメモリをかなり「贅沢」に使っています。1つ1つのオブジェクトには「キーの名前」や「管理用のメタデータ」が付随するため、数が増えると無視できない重さになります。
そこで、現代の高度なシステムでは、これらの情報を**「たった1つの64bit整数」**の中に無理やり詰め込む手法がとられます。
2. ビットパッキング:数字を「引き出し」にする
「1つの数字にどうやって複数のデータを入れるの?」と思うかもしれません。
イメージとしては、64個のスイッチが並んだ長いパネルを想像してください。
「0番目から10番目まではHP用」「20番目から22番目までは状態フラグ用」というように、ビットの範囲(区間)ごとに役割を決めるのです。
メモリレイアウトのイメージ
1つの64bit整数を、定規で測るように区切ります。
- 右端の16マス: 現在のリズムや進み具合
- 次の16マス: エネルギーや強さ
- 特定の数マス: 誕生、成長、休憩などの「状態」
- 余った1マス: 今動いているかどうかの「スイッチ(フラグ)」
このように、情報を**「0か1の並び」**として隙間なくパッキングすることで、メモリ消費量を劇的に抑えられます。
3. 実装のコツ:ビット演算を「型抜き」に例える
JavaScriptでこれを扱うには、BigInt(64bit整数を扱える型)とビット演算を使います。
情報を「詰め込む(Pack)」
特定のデータを「左に○回ずらして合体させる」という操作をします。
let packed = 0n; // 64bitの空箱
let status = 3n; // 例えば「元気」という状態
// 48番目の場所まで「ずらし」て、元のデータと重ね合わせる!
packed |= (status << 48n);
情報を「取り出す(Unpack)」
逆に、特定の場所だけを「型抜き」するように取り出します。
// 右にずらして、必要な部分だけを「マスク(遮断)」して抽出
let currentStatus = (packed >> 48n) & 0xFn;
一見すると呪文のようですが、やっていることは**「定規で測って、必要な部分だけを切り取る」**という非常にシンプルな理屈です。
4. なぜこれが「速さ」に直結するのか?
単にメモリを節約するだけでなく、この手法には強力なメリットがあります。
-
GPUにそのまま送れる:
WebGPUなどの最新技術を使う場合、この「数字の列」をそのままビデオメモリへ高速転送できます。10万個のバラバラなオブジェクトを送るのとは比較にならない速さです。 -
「サボる」のが得意になる:
「今は休憩中」というフラグを1ビット忍ばせておけば、計算機(GPU)側で「この数字は計算しなくてOK!」と瞬時に判断でき、無駄な演算を極限までカットできます。 -
どこでも同じ結果が出る:
浮動小数点(0.123...)のような曖昧な計算を排除し、すべて「整数」のビット操作で完結させることで、どんなスペックのPCでも1ミリも狂わない同じ結果(決定論)を再現できます。
まとめ:見えない場所を「整える」美学
私たちが普段使っているリッチなWebコンテンツや複雑なシミュレーションの裏側では、こうした**「数字を限界まで詰め込む」**工夫が凝らされています。
「オブジェクトが重いな」と感じたら、一度ビットの深淵を覗いてみてください。たった1つの数字が、1つの生命や宇宙を表現する器に見えてくるはずです。
Next Step: 次回は、この整えられたデータを使って、10万個の衝突判定を爆速で終わらせる「空間管理の魔法」についてお話しします。
用語ミニ解説
- BigUint64Array: 64bitの符号なし整数を大量に並べるための配列。JavaScriptで低レイヤーなデータ処理をする際の強い味方。
- 決定論: 同じ入力なら必ず同じ結果になること。マルチプレイゲームなどの同期には欠かせない考え方。