🐍

Pythonのコンパイラを作りたい #6 - ランタイムとメモリ管理

2025/02/13に公開

こんにちは。前回 (「#5 - リストと辞書の実装」) では、Python 的なリストや辞書を C 言語でどのように再現しているか、その実装の概要をご紹介しました。
今回のテーマは、ランタイムとメモリ管理です。
リストや辞書などのオブジェクトを動的に生成していく過程で、どのようにメモリを確保・解放しているのか、そして なぜ Boehm GC (ガーベジコレクタ) を導入したのか について具体的にお話しします。

1. 当初は自前でメモリ管理していた

1-1. ポインタの連鎖が増えて混乱した

開発初期には「C 言語で書くんだから、mallocfree をうまく使えば十分だろう」と考えていました。しかし、Python の世界観をある程度再現しようとすると、以下のような複雑さがすぐに表面化します。

  1. リストや辞書の中に多様なオブジェクトが入り、それぞれがまた別の構造体を参照している
  2. クラスや関数オブジェクト、文字列なども増えてくると、どこで解放すべきかのタイミング判断が困難
  3. 参照が循環しているケース (例: A が B を参照し、B が A を参照する) をどう解決するか

こうしたポインタの連鎖が増えると、「どのタイミングで free() を呼ぶのが正解か?」 を管理するのが非常に大変になります。あるオブジェクトを解放してしまうと、別のオブジェクトから参照されている可能性があるかもしれないし、逆に解放し忘れるとメモリリークが起きるかもしれない...。

1-2. 参照カウントやマニュアルフリーに苦戦

  • 参照カウント: CPython のように各オブジェクトが refcount を持ち、参照が増えたら加算・減ったら減算する仕組みを試みました。しかし、循環参照があるとこれだけでは解放されず、ゴミが残ってしまいます。
  • 手動の free(): 全オブジェクトのライフサイクルを完全に把握しながら free() を呼ぶアプローチも考えましたが、たとえば辞書やリストをコピーするときやリサイズするときなど、どの段階まで生き残るのか判断を誤りやすく、クラッシュやリークが多発しました。

結果的に、「いっそ GC (ガーベジコレクタ) を導入すればもっと楽になるのでは?」 という結論に至りました。ちょうど C/C++ 向けの Boehm GC が比較的有名であり、導入が簡単そうだったのも大きな要因です。

2. Boehm GC を導入したメリット

2-1. メモリ解放を意識しなくてよくなる

Boehm GC は Hans Boehm 氏らによって開発された マーク&スイープ方式のガーベジコレクタライブラリです。
C/C++ の世界で「GC が欲しいけど自作したくない」というニーズに応えるべく、多くのプラットフォームで動作し、成熟度が高いのが特徴です。malloc の代わりに GC_malloc を呼ぶだけで、不要になったオブジェクトを自動で回収してくれます。

これによって、オブジェクトを生成したあと「どこで free() するか」考えずに済むようになります。とりあえずポインタが見えなくなったら GC が「これはもう参照されていないな」と判断して解放してくれるので、循環参照 も自然に解決されるのが大きなメリットです。

2-2. リストや辞書の設計が劇的に簡単に

前回の記事で紹介した PyListPyDict は、要素数を動的に増やすときに新しい配列を GC_malloc して古い配列を放置します。しかし、GC があるおかげで、古い領域をいちいち free() しなくても問題ありません。
特に辞書の再ハッシュ (リサイズ) の段階で、バケット配列を拡大するときに新配列を作り、旧配列の要素をコピーし終わったあと、旧配列を解放するかどうかを考える必要がなくなります。GC を使わずにこれをやると、誤って別の参照が残っているかもしれず神経を使うところです。

2-3. 性能への影響は?

「GC = 遅い」というイメージを持つ方も多いですが、本プロジェクトのように比較的小規模な環境であれば、そこまでパフォーマンス問題は目立ちません。

  • オブジェクトの生成ペースがそこまで爆発的でない
  • Boehm GC は古くからあり、安定性や実績がある
  • 参照カウントをバリバリ回したり、手動で free() を呼ぶ頻度が増える方が、コードが複雑になる上に高速化も難しい

もちろん大規模アプリケーションで GC のポーズ時間が影響するケースもあるとは思いますが、少なくともコンパイラ実験的プロジェクトとしては十分に使い勝手がよく、開発コスト > 性能のわずかなデメリット と判断しました。

3. Unix 環境を前提とした導入手順

3-1. インストールとリンク

Boehm GC は多くの Linux ディストリビューションや macOS でパッケージ化されており、簡単に導入できます。例えば:

  • Debian/Ubuntu系: sudo apt-get install libgc-dev
  • macOS/Homebrew: brew install bdw-gc

その後、コンパイル時には以下のように pkg-config --cflags bdw-gcpkg-config --libs bdw-gc を使ってインクルードパスとリンクを指定すればOKです。

CFLAGS = -Wall -Wextra -O2 $(shell pkg-config --cflags bdw-gc)
LDFLAGS = $(shell pkg-config --libs bdw-gc)

runtime.o: ...
    $(CC) $(CFLAGS) -c ...

3-2. コード修正

  • malloc(size) -> GC_malloc(size)
  • calloc(n, size) -> GC_malloc or GC_malloc_atomic + 手動の memset (オブジェクトにポインタが含まれない場合は GC_malloc_atomic で最適化可能)
  • free(ptr) は呼ばないでOK
  • 大抵の場合、GC_init() はライブラリ内部で自動呼び出しされるが、明示的に呼んでもよい (pyc では @main 関数の冒頭で呼び出している)

このように、C コードを書き換える工数はそれほど大きくありません。もちろん、ポインタを使っている箇所を再点検して修正する必要はありますが、設計そのものは大幅に単純化できます。

3-3. Windows や他プラットフォームでの扱い

Boehm GC は Windows 版も存在しますが、ビルドや依存ライブラリなど少々注意が必要です。このプロジェクト自体は Unix 系 OS (Linux, macOS) を主なターゲットにしているため、まだ Windows のサポートは十分ではありません。
今後ポータビリティを高めるには、CMake などでビルド環境を整理したり、Boehm GC を含めてバイナリ配布する形にするなど工夫が求められるでしょう。

4. WebAssembly向け GC (Wasm GC) への期待

4-1. 将来的にブラウザ上での動作を目指す

pyc の目的の一つに「Python を WebAssembly へコンパイルしてブラウザ上で動かす」という構想があります。
現状でも Clang + LLVM を使えば .ll -> .wasm への変換自体はできますが、問題は メモリ管理。ネイティブ用に書いた GC がそのまま WebAssembly 上で動くかというと、制限があったりフル機能が使えない場合があるため、まだハードルが高いです。

4-2. Wasm GC の標準化状況

Wasm GC (Garbage Collection) の拡張は、2024 年時点でまだ草案・実験的段階と言えます。将来的にブラウザのネイティブ機能として GC が利用できれば、Boehm GC のようなものを自前でリンクしなくても済むかもしれません。ただ、現時点ではそこまで成熟しておらず、大規模なサポートはこれからという状態です。

4-3. 実験的な取り組み

すでにいくつかのコミュニティでは、Boehm GC を Emscripten/wasm で動かす試みがあります。このプロジェクト pyc でも、いずれは「WebAssembly 版のランタイムをビルドして、ブラウザで軽量 Python コードが動く」というシナリオを実験できればと考えています。
Pyodide のように CPython まるごと移植するよりは、ずっと小さなランタイムを目指せる点が利点となるかもしれません。

5. その他の GC 選択肢

Boehm GC は歴史が長い分、非常に安定していて情報も豊富です。一方で、C/C++ 用に他のトレース GC ライブラリが存在する場合もあり、例えば libgc++ といったものが試作されていたり、あるいは自身でマーク&スイープを実装する猛者もいます。
ただし、このプロジェクトの方針としては「なるべく車輪の再発明を避ける」という意図で、Boehm GC を採用しました。参照カウントや自作 GC などは面白く学びにはなる一方、バグが多発する可能性が高いからです。

6. まとめ & 今後の話

今回は、Boehm GC の導入経緯メリット、そして WebAssembly での展望 を中心にお話ししました。

  • 自前メモリ管理の挫折: リストや辞書のように可変・混在型のデータ構造を手作業で解放するのは難しすぎる。
  • Boehm GC に頼る: GC_malloc でオブジェクトを作るだけで自動解放され、循環参照も気にしなくてよい。
  • パフォーマンスもそこまで大きな問題ではない: 小規模であれば十分実用的。
  • Wasm GC への期待: 将来的にはブラウザ上で Python コードを動かす際にネイティブ GC を利用できるかもしれないが、まだ道半ば。

こうした流れを経て、pyc のランタイムは「C 言語 + Boehm GC」である程度気軽に Python 的オブジェクト管理ができる基盤が整いました。

次回は、「ベンチマークと最適化」を取り上げます。具体的には 単純なフィボナッチ計算 を例に比較を行い、CPython / C / LLVM IR / JavaScript (Node.js など) といった複数の言語の実装を比較・測定する仕組みを簡易的に組み込んでいるので、その結果を紹介しながら pyc の実力 (や課題) を見ていこうと思います。

次回:
https://zenn.dev/t3tra/articles/0315b76fd987d2

Discussion