Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
不幸な GC
Search
Chen
August 22, 2026
Programming
180
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
不幸な GC
Chen
August 22, 2026
More Decks by Chen
See All by Chen
約 5 年のゲーム開発 ~リリースまでがこんなに大変でした~
chencmd
1
710
fs2-io を試してたらバグを見つけて直した話
chencmd
0
920
Scala におけるコンパイラエラーとの付き合い方
chencmd
2
1.6k
Language Server Protocol の歴史について (a.k.a. 考古学者になる方法)
chencmd
1
180
Other Decks in Programming
See All in Programming
レビュー履歴をAIに食わせて、 Compose移行を加速するs
shihochan
0
160
FastAPI の並行処理モデルを完全に理解する
hoto17296
1
370
Dockerfile CMD for Node.js
grazie1999
0
110
Discordを用いたラボオートメーション関連情報収集の自動化
noguhiro2002
0
140
Japan Community Day at Kubecon + CloudNativeCon Japan 2026: Learning Container Privilege Control by Building My Own Low-Level Container Runtime
ternbusty
1
160
MySQLとPostgreSQLって何が違うの?
akagami
0
120
AIと壁打ちしながら進めるコスト管理
fufuhu
0
1k
What's New in Android 2026
veronikapj
0
270
FDEが実現するAI駆動経営の現在地
gonta
2
290
「つくるAI」だけではバグは見つからない ~テストに必要な「見つけるAI」を分離させる戦略~
mfunaki
0
120
PHP に部分適用が来るぞ!……ところで何それ?おいしいの? #phpcon / phpcon-2026
shogogg
0
720
2年かけて Deno に DOMMatrix を実装した話 / How I implemented DOMMatrix in Deno over two years
petamoriken
0
210
Featured
See All Featured
Chasing Engaging Ingredients in Design
codingconduct
0
280
Reflections from 52 weeks, 52 projects
jeffersonlam
356
21k
Redefining SEO in the New Era of Traffic Generation
szymonslowik
1
390
Lightning Talk: Beautiful Slides for Beginners
inesmontani
PRO
2
640
世界の人気アプリ100個を分析して見えたペイウォール設計の心得
akihiro_kokubo
PRO
73
41k
A Tale of Four Properties
chriscoyier
163
24k
Ruling the World: When Life Gets Gamed
codingconduct
0
300
How Fast Is Fast Enough? [PerfNow 2025]
tammyeverts
3
790
We Are The Robots
honzajavorek
0
300
Exploring the relationship between traditional SERPs and Gen AI search
raygrieselhuber
PRO
2
4.2k
A Modern Web Designer's Workflow
chriscoyier
698
190k
Learning to Love Humans: Emotional Interface Design
aarron
275
41k
Transcript
不幸な GC ちぇん / Chen 2026/08/22 Kernel/VM 探検隊@東京 No19
世 界 あやしい VM って? 2/53
世 界 あやしい VM って? 「配列はあるのに動的インデックスアクセスがほぼできない」 とか・・・ 3/53
世 界 あやしい VM って? 「配列はあるのに動的インデックスアクセスがほぼできない」 とか・・・ 「データの取り扱いがめちゃくちゃ下手」 とか・・・ 4/53
世 界 その VM の名は 5/53
世 界 その VM の名は Minecraft ブロックでできたオープンワールドの 世界で探検したり、建築したり、モン スターと戦ったりできる VM
そこそこの表現力を持つコマンド体系 が標準で搭載されている 6/53
君誰? 名前: ちぇん / Chen Twitter: @Chen__TS GitHub: @ChenCMD 本業:
サーバーサイドエンジニア / 技術広報 メイン言語: Scala 人生を狂わせたもの 趣 味 : Minecraft Minecraft のコマンド体系に 9 年在住 メンバー 30 人以上を率いて Minecraft 上で ゲームを約 6 年間作っている 7/53 https://project-tsb.org
そんな世界で何に困っているのか? 何を作るにしても、entity に固有の状態を持たせたくなる entity: プレイヤー / モブ / 落ちているアイテム /
放った矢 / 残留ポーションのモヤモヤ など、ワ ールド上に存在するオブジェクトだいたい全てのこと 例えば・・・ 独自のパラメータ AI の内部状態 Temporary なフラグやリスト 8/53
幸運な世界ならこう書くよね entity.data = { name: "checche", age: 222.2, history: [12,
42, 51] } データの観点で言えば 配列やオブジェクトも持てる entity の観点で言えば entity ごとに独立している 好きなだけ構造化できる これくらいはほしい! が、Minecraft にはこれらを満たす仕組みが標準では存在しない 9/53
使えるもの (1/2) scoreboard Map<EntityID, i32> 相当のグローバルなデータ領域 ここが嬉しい! entity が消滅した際に対応するレコードが それ以外はだいたい何も出来ない
自動で回収される entity が消滅した際に対応するレコードの 特定レコードに対する四則演算・剰余が 値を使ってなんやかんやするとか できる 値が N であるレコードが存在するかを確認 全レコードを対象とする、定数加算などの 算術演算 全レコードの min / max が取れる 10/53 ここが嬉しくない! するとか bit 演算をするとか
使えるもの (2/2) StorageObject JSONObject 相当のグローバルなデータ領域 ここが嬉しい! JSONPath ライクな pathでデータへアクセ スできる
配列に対して 「末尾から N 番目の要素」 とい う表現を扱える 11/53 ここが嬉しくない! 動的インデックスアクセスができない 参照を取るという操作がなく、値を peek す る以上のことをすると deep コピーになる
つまり? Map<Entity, StorageObject> が欲しい! それを実現しているライブラリがある 12/53
つまり? Map<Entity, StorageObject> が欲しい! それを実現しているライブラリがある Oh!MyDat! さっき欲しいと言ったものを、scoreboard と StorageObject だけで実現しているラ
イブラリ 気持ち的にはこんなイメージ entity A -> { name: "checche", age: 222.2, history: [12, 42, 51] } entity B -> { ... } entity C -> { ... } scoreboard と StorageObject から、どうやってそんなものを・・・? 13/53
まずは I/F を見てみよう Oh!MyDat! が提供するのは、実質下記の 1 つのみ please(Entity): void 呼び出す前の状態に関わらず、特定の
path が引数として渡した Entity の データ領域を指すようになる魔法の関数 please(entityA) Storage.write(path = "data[-4][-4][-4][-4][-4][-4][-4][-4].x", value = "neko") please(entityB) Storage.write(path = "data[-4][-4][-4][-4][-4][-4][-4][-4].x", value = "dog") please(entityA) Storage.read(path = "data[-4][-4][-4][-4][-4][-4][-4][-4]") // => { x: "neko" } 14/53
Q. この data って何? A. ただの深さ 8 の (完全とは限らない) 4
分木 ⁸ 4 = 65536 個ある末端ノードが entity のデータ領域 データ構造としても、ただのネストされた配列 [[[[[[[[{}, ...], ...], ...], ...], ...], ...], ...], ...] entity に木のパス (アドレス) を対応させる方法さえ用意す れば末端ノードと entity の紐付けができる うれしい! 15/53
そろそろお忘れかも知れませんが・・・ おさらい StorageObject ここが嬉しくない! 動的インデックスアクセスができない 参照を取るという操作がなく、値を peek する以上のことを すると deep
コピーになる こ、こんな制約下でどうやって木を扱えと・・・ 16/53
ここで data[-4][-4]...[-4] が再登場 特定の path が引数として渡した Entity のデータ領域 を指すようになる おさらい
StorageObject ここが嬉しい! 配列に対して 「末尾から N 番目の要素」 という表現を扱える つまり: data を配列と仮定して、「末尾から 4 番目を対象 にするという操作」 を 8 回繰り返している ≒ data.get(-4).get(-4) ... .get(-4) 17/53
つまり•••? 動的インデックスアクセスができないなら、 木の方を変形すれば良い path は data[-4][-4]...[-4] で固定する 末尾に 0~3 個の空ノードを追加すると、
[-4] が指す枝を変えられる これを 8 段繰り返せば、path を固定したまま 任意の末尾ノードにアクセスすることができる 18/53
つまり•••? 動的インデックスアクセスができないなら、 木の方を変形すれば良い path は data[-4][-4]...[-4] で固定する 末尾に 0~3 個の空ノードを追加すると、
[-4] が指す枝を変えられる これを 8 段繰り返せば、path を固定したまま 任意の末尾ノードにアクセスすることができる つまり? 深さ 8 の 4 分木 深さ 8 の 4 ~ 7 分木 19/53
please 完全に理解した 20/53
大事なことを忘れていませんか•••?
不幸な GC ちぇん / Chen 2026/08/22 Kernel/VM 探検隊@東京 No19
あ゛!!!!! 確かに entity に紐付く固有のデータ領域は出来た が、今のままでは entity が消滅しても紐付くデータ領域が消えない 23/53
あ゛!!!!! 確かに entity に紐付く固有のデータ領域は出来た が、今のままでは entity が消滅しても紐付くデータ領域が消えない Q. entity が消滅した時に消せば良いだけでは?
24/53
あ゛!!!!! 確かに entity に紐付く固有のデータ領域は出来た が、今のままでは entity が消滅しても紐付くデータ領域が消えない Q. entity が消滅した時に消せば良いだけでは?
A. そんな幸福な世界だったらよかったですね (笑) この世界において、イベントハンドラみたいな幸福なものはほぼ存在しない^1 当然、entity の消滅を元になんやかんやするなどできるはずもなく••• やりたきゃこちら側でその仕組みを工夫して実現する必要がある 25/53
では逆に何があれば GC ができるのか? 消滅した entity に割り当てられていた address が分かれば、 対応する末端ノードを空に戻せる でも
entity が消滅した時点で、その対応関係も失われてしまう → 直接どうにかするのは難しそう••• 26/53
こんな時は一般の世界のありがたい言葉を参照しよう Fundamental theorem of software engineering ソフトウェア工学の基本定理 「コンピュータ科学のあらゆる問題は、間接参照 (indirection) のレイヤーを
1 つ追加することで解決できる」 — David Wheeler こんな世界でももちろんソフトウェア工学の基本定理は適用できるはずだ please(Entity) が外向きに保証しているのは entity から個別データ領域への 対応だけ 木のどの末端を使うかや、その対応をどう管理するかは実装側で自由に決められる ということで、entity と 木の間に entity が消滅したことがわかるような中間層を挟 んで、解決しよう 27/53
Q. どこに中間層を生やすって? A. ここ 28/53
中間層を用意する 我々が勝手に割り当てる 16 bit address の保存先を 2 カ所に用意する scoreboard Map<Entity,
address> StorageObject: addresses SomethingCollection<address> レコードのライフタイムが entity と同期 entity のライフタイムとは無関係に する address を保持する A B C 29/53 → 0xC000 → 0x8000 → 0x4000 0xC000 0x8000 0x4000
このように保存して entity が消滅すると何が起きるか 消滅前 entity B が消滅した後 scoreboard StorageObject scoreboard
StorageObject A → 0xC000 0xC000 A → 0xC000 0xC000 B → 0x8000 0x8000 C → 0x4000 0x4000 0x8000 C → 0x4000 0x4000 scoreboard 側だけが entity と一緒に変化するので、2 つの状態に差が生じる entity の消滅そのものは観測できないが、この差分を根拠にすれば、 消滅した entity に割り当てていた address を特定できるという言えるだろう じゃあその状態の差分取れば GC も実装終わりだね!楽勝ですね! 30/53
ちょっと待って•••? 31/53
ちょっと待って•••? おさらい scoreboard ここが嬉しくない! それ以外はだいたい何も出来ない 値が N であるレコードが存在するかを確認するとか 32/53
ちょっと待って•••? おさらい scoreboard ここが嬉しくない! それ以外はだいたい何も出来ない 値が N であるレコードが存在するかを確認するとか 差分が取れないじゃん 33/53
では、GC は実装できないのか? 全 address と全 entity を突き合わせるような全精査は不可能 34/53
では、GC は実装できないのか? 全 address と全 entity を突き合わせるような全精査は不可能 なら、差分の一部だけを検証する形なら、あるいは•••? 35/53
これをやるのに極めて都合がいい構造がある StorageObject 側の address collection を少し頑張る 具体的には••• 0〜65535 の address
を保持する ある位置から見ると、円環状に降順になる 「次に掃除する場所」を 1 箇所だけ持つ この位置を head と呼ぶ この構造を addressRing と呼ぶ 36/53
この ring をどう使う? 例えば、 0xC000 、 0xA000 、 0x4000 が消滅しているとする
この図上では、 head は 0xC000 となる 37/53
この ring をどう使う? 例えば、 0xC000 、 0xA000 、 0x4000 が消滅しているとする
この図上では、 head は 0xC000 となる scoreboard を用いて head 以下で生きている 最大の address が 0x8000 だと分かったとす る 38/53
この ring をどう使う? 例えば、 0xC000 、 0xA000 、 0x4000 が消滅しているとする
この図上では、 head は 0xC000 となる scoreboard を用いて head 以下で生きている 最大の address が 0x8000 だと分かったとす る すると、 0x9000 に到達するまでに通った address は全部消してよいことがわかる やった〜 できた〜 39/53
この ring をどう使う? 例えば、 0xC000 、 0xA000 、 0x4000 が消滅しているとする
この図上では、 head は 0xC000 となる scoreboard を用いて head 以下で生きている 最大の address が 0x8000 だと分かったとす る すると、 0x9000 に到達するまでに通った address は全部消してよいことがわかる やった〜 できた〜 40/53
そんな演算あるわけがない 41/53
そんな演算あるわけがない 欲しいのは雰囲気で言えばこれ scoreboard.records .filter(n => n <= head) .max() しかし使えるのはせいぜい
全レコードに同じ値を足す / 引く 全レコードの max を取る filter なんて幸福なものは存在しない 42/53
いったん Minecraft を忘れる 0〜65535 の範囲にある i32 値の集合 threshold を取りたい 以下の最大値
ただし使えるのは 集合全体への一括加減算 集合全体の max だけとする 43/53 から
こうやる (AI slop) https://claude.ai/public/artifacts/3bf1cf38-7f15-45e4-bee8-c76505a60f10 44/53
Minecraft に戻ってきて••• scoreboard が持っていた機能を思い出す 全レコードへの一括加減算 全レコードの max なので実際に 1. scoreboard
全体に i32_MAX - head を加算する 2. scoreboard 全体の max を取る 3. 取得した max と scoreboard 全体から i32_MAX - head を引く とすれば head 以下で生きている最大の address、即ち GC の境界 が取れることがわかる 45/53
ring に戻ってみよう 例えば得られた境界が 0x9000 だとする 1. addressRing の先頭を peek する
2. 境界値以下であるなら pop し、その address の領域を空にする 3. このループ^2を 2 の条件が満たされる限り回し続ける 4. 最後に、次回の GC で別の区間を見るために head を一つ進める できた!!!!!! 46/53
まとめ (1/4) 動的 index がない → path を動かさず、木の方を変形する 47/53
まとめ (2/4) 動的 index がない → path を動かさず、木の方を 変形する entity
の消滅をフックできな い → ライフタイムの異なる 2 つ の状態を用意して、そこに生 じる集合としての差分を利用 する 48/53
まとめ (3/4) 動的 index がない → path を動かさず、木の方を変形する entity の消滅をフックできない
→ ライフタイムの異なる 2 つの状態を 用意して、そこに生じる集合としての 差分を利用する 差集合も条件付き max もない → 全てのゴミを探さず境界問題に変換 し、i32 の値域を回して普通の max に する 49/53
まとめ (4/4) 動的 index がない → path を動かさず、木の方を変形する entity の消滅をフックできない
→ ライフタイムの異なる 2 つの状態を用意して、そこに生じる集合としての差分 を利用する 差集合も条件付き max もない → 全てのゴミを探さず境界問題に変換し、i32 の値域を回して普通の max にする できない操作を求める代わりに、問題の形を変える そんな形で Minecraft のコマンド体系で人々は過ごしているのでした おしまい 50/53
謝辞 テストに付き合ってくださった方々 (敬称略) @yanorei32, @syugasato, あにみゃん, @Tenasub2021 登壇資料作成を手伝ってくださった方々 @Kory__3, @hsjoihs
昨日の深夜 2 時半に唐突に頼んだクリーパーのイラストを 7 分で書いてくれた方 @nukknu99 51/53
不幸な GC ちぇん / Chen 2026/08/22 Kernel/VM 探検隊@東京 No19
おまけ: この ring はどう維持する? 新しい entity に渡す address は実装側で自由に決められた そこで
allocation のたびに GC を走らせて head を進める head と末尾の address の間に空きを作る 末尾が 0 の場合は 0x10000 として扱う その中間値を新しい address として末尾へ追加する こうして、 head から見て降順になるという条件を保ったまま address を増やせる 53/53