
2026/05/29 3:16
孤独なLispのヒープ
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
「Lone」 Lisp インタプリータの中核にある物語は、非効率なカスタムアロケーターから、高性能で現代的なヒープ構造へと進化した点にあります。約 3 年間、システムはリンクリストベースのアロケーターを使用してきており、これが配分に対して線形スキャンを行っていたため、小さな要求に対して大きなブロック(最大 16 KiB)を使用しており、結果として 30%–50% のメタデータオーバーヘッドを発生させていました。分割と結合はこれらの非効率性を軽減しましたが、根本的な問題の解決にはなりませんでした。また、すべての Lisp 値ポインタのリストが必要で複雑なスタンスキャン問題を招くため、保守的なガバジーコレクションが状況をさらに複雑にしていました。これらのボトルネックに対処するために、開発者はタグ付きポインタを単一の大きな配列への平坦なインデックスに置き換えるとともに、個々のオブジェクトのアロケーションから 64 バイトに aligned な
mmap を用いたヒープへの移行を行いました。これによりページ境界を越えませんでした。初期の抵抗にもかかわらず、キャッシュフレンドリーであるためリンクリストではなく配列が採用されました。設計ではポインタが無効化されないようにするため、直接のインプレースリサイズを廃止し、代わりに新しい配列を割り当て、データをコピーして古いものを破棄する方式へと移行しました。拡張時には、データをコピーせずに容量を拡大しページを移動させる linux_mremap を使用しています。これらの向上により、メモリアクセシの断片化とオーバーヘッドが最小化され、malloc や libc などの重い依存関係を避けることができます。これにより、企業向けに最適化され軽量なインタプリータを提供し、プロセッサ効率を最大化することが可能になります。本文
Lone: 動的言語の「副産物」となる進化の旅
多くの動的言語同様、Lone も当初はシンプルに始まりました。その実質は、C 言語で記述されたデータ構造の集合体です。本プロジェクトの唯一の目的は、以下の要素を結びつけることにありました:
- すべての型を統括する ユニオン
- メタデータを内蔵した タイピング済みの値構造
- これらを「動作可能なプログラム」へと繋ぐ カスタム言語
1. スタートライン:「Nothing」から始まり、「One」へ至る
当初、世界という過酷な現実に対し、あまりにも 素直(naive) かつシンプルな思想に惹かれました。その反映とも言えるのがコードの初期形態です。
メモリー管理の独自実装
C 言語で記述する際、標準的な動的メモリー割当機能(
malloc など)や libc は存在しません。**「私とコードのみ」**という状態でした。
そのため、独自のメモリーアローテーターを実装することになりました。
// データの基本情報:場所(位置)とサイズ struct lone_memory { size_t size; unsigned char pointer[]; }; // 「自由か」「使用中か」の追跡が必要 struct lone_memory { bool free; size_t size; unsigned char pointer[]; }; // リンクリスト化により探索可能に(First Fit 方式) struct lone_memory { struct lone_memory *prev, *next; bool free; size_t size; unsigned char pointer[]; };
割当と解放のロジック
- 割当: リンクリストを探索し、適合する最初のブロックを使用(
)。First Fit - 分割: 使用済みのブロックは残りを新たな自由ブロックとして分割。
if (excess >= sizeof(struct lone_memory) + 1) { new = (struct lone_memory *) (block->pointer + size); new->free = true; new->size = excess - sizeof(struct lone_memory); block->size = size; // 元のサイズを更新 } - 解放: ブロックを
にするだけでなく、隣りのブロックとも併せて統合(コエレスス)する処理も行う。free
2. パフォーマンスの限界と課題
この自作アローテーターはシンプルではありましたが、以下のような重大な欠陥を持っていました:
- 線形探索: すべての割当ごとにブロックリストを回すため非効率。
- メモリーの浪費: メモリーフラグメンテーション(破片化)が抑制できず、OOM(Out of Memory)になる可能性大。
- メタデータオーバーヘッド: ブロック記述子を前付けさせることで、通常割当時でも 30%〜50% のオーバーヘッドが発生。
しかし、このありえない仕組みで約 3 年間 ランディングし、必要なオブジェクト作成は賄ってきました。本質的には「値さえあれば」よかったためでした。
3. ガーベージコレクターとポインタの専制政治
Lone のガーベAGER コレクターは「保守的(conservative)」です。スタックを辿り、すべての Lis オブジェクトへのポインタ を厳しく精査する必要があります。
当初はスタック内の全ワードを全ポインタと比較するアプローチが考えられましたが、それは $O(N^2)$ の計算量となり現実的ではありませんでした。解決策として以下の工夫が必要となりました。
「第一のヒープ」への移行
汎用メモリーアローテーターのような保証ではなく、言語自体がまとめて購入(確保)する方式に変更しました。
struct lone_lisp_heap { struct lone_lisp_heap_value values[LONE_LISP_HEAP_VALUE_COUNT]; };
- 仕組み: オブジェクトは個別に割当せず、ヒープという「巨大な配列」内のスロットとして管理。
- ガベージコレクション: 「解放(dealloc)」するのではなく、死んだ値のみにマーク(殺す)を施すだけ。これによりスタックからの参照確認が簡素化される。
ポインタの問題点と解決策
しかし、巨大な配列には新たな問題がありました:
- インサート不可: チャンクの真ん中に値を挿入するにはリサイズが必要(破壊的再構築)。
- ポインタの専制政治: 既存のすべてのポインタが新しいメモリー領域において無効化され、ダングリング・ポインタ を招く。
これらの問題を解決するため、以下の決断を下しました:
- 根本的な変更: すべての値表現を再記述。
- ポインタ廃止: 「カンフーポインタ」から、Lone ヒープへのインデックスへ移行。
4. 完全なヒープ設計:ページ単位での管理
現在、Lone の値は配列へのインデックスとして扱われ、メモリー位置に依存しなくなりました。これにより、ヒープの再割当や移動が容易になりました。
キャッシュフレンドリーなデザイン
- 値サイズを 56 バイト から 64 バイト に整列(キャッシュラインサイズに合わせる)。
- ヒープ全体を巨大なページとして
で確保。mmap
void lone_lisp_heap_initialize(struct lone_lisp *lone) { size_t size = LONE_LISP_HEAP_CAPACITY * sizeof(struct lone_lisp_heap_value); // Linux 固有の mremap の利用で柔軟なページ管理を実現 lone->heap.values = linux_mmap(0, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); lone->heap.count = 0; }
メモリー拡張と移動 (mremap
)
mremaplinux_mremap を使用することで、ページベースのヒープを効率的に拡大・移動できます。メモリのコピーを行うことなく、単にテーブルを更新するだけで済みます。
static void lone_lisp_heap_grow(struct lone_lisp *lone) { size_t old_capacity = lone->heap.capacity; size_t new_capacity = old_capacity * 2; // MREMAP_MAYMOVE オプションにより、メモリーが移動しても OK lone->heap.values = linux_mremap(lone->heap.values, old_capacity * sizeof(...), new_capacity * sizeof(...), MREMAP_MAYMOVE); lone->heap.capacity = new_capacity; }
現状の評価
Lone のヒープは、「黒いモニュメント」(巨大で愚かで平坦な配列)へと変貌しましたが:
- ✅ 位置非依存: インデックスによるアクセスのため、メモリー移動しても動作不变。
- ✅ 効率性: 値単位の割り当てからページ単位へ進化し、
を活用した高速な拡張が可能に。mremap
結論:「Nothing」から「One」、そして「Infinite」へ
- Nothing(何もない): 独自実装のメモリーアローテーターでスタート。シンプルだが非効率。
- One(一): ガーベージコレクターのためのヒープ構造を完成させ、ポインタ制約を打破。
- Infinite(無限)への挑戦: 現在は線形探索によるガベージコレクションを行っており理論的には不完全。しかし、この仕組みで三年間稼働し、「値さえあれば」何とかなった実績があります。
次は、ゼロ番目のオブジェクトが常に生存しているという前提を捨て、完全に動的な処理へ。それが「無限」への第一歩となります。