RubyのGCを深掘りする(1)GC::INTERNAL_CONSTANTS(翻訳)
昔から「ゴミを見ればその人がわかる」と申しますが、プログラミング言語でも同じことが言えます。より正確には、プログラミング言語のガベージコレクションを調べると、そのプログラミング言語について多くのことを学べます。本記事は、RubyのGCシリーズ記事の第1回です(最終的には書籍にします)。
今回は(イマイチなメタファーですが)、Rubyのゴミ箱の中に必ずあるものをあさってみるところから始めます。RubyはGC::INTERNAL_CONSTANTSをpublicにしているので、これを使ってガベージコレクタの各種定数を調べられます。
GC::INTERNAL_CONSTANTS
Ruby 3.0でGC::INTERNAL_CONSTANTSを出力すると以下のようになります。
{
:DEBUG=>false,
:RVALUE_SIZE=>40,
:HEAP_PAGE_OBJ_LIMIT=>409,
:HEAP_PAGE_BITMAP_SIZE=>56,
:HEAP_PAGE_BITMAP_PLANES=>4,
:HEAP_PAGE_SIZE=>16384
}
これらの定数を1つずつ見ながら定義することで、Rubyがどのようにメモリを構成しているかをさらに詳しく知ることができます。
:DEBUG => FALSE
:DEBUG という定数は、Rubyでプログラムを書くときのデバッグよりも、Rubyそのもののソースコードを書くときのデバッグに便利です。実行するほとんどのRubyプログラムは:DEBUG => FALSEになっているので、通常気にする必要はありません。
興味しんしんの方向けに説明すると、:DEBUGがTRUEになるのは、Rubyのソースコードをコンパイルする際に、cppflagでDGC_DEBUGをtrueに設定した場合のみです(参考)。しかしRubyのソースコードをコンパイルすることは通常ないので、ほとんどの場合FALSEになります。
:RVALUE_SIZE => 40
最初の重要なポイントは、*_SIZEで終わる定数がすべて「バイト単位」になっていることです。つまり定数:RVALUE_SIZE => 40は、RVALUEが40バイトに設定されていることを示します。
なるほど、ところでRVALUEとは何かが気になってきますね。RVALUEはRubyオブジェクトの基本的な情報を格納します。RVALUEは、RubyオブジェクトをCで表現するさまざまなCの構造体が共用体(union)になったものです(参考)。
RVALUEには「Rubyオブジェクトの実際の値」という情報が含まれますが、この値が40バイトの制限を超える場合は、代わりにRubyオブジェクトの値がOSのヒープ上のどこにあるかを示すポインタが含まれます。
(細かい話ですが、文字列が23文字以下の場合はRVALUESの中に値が格納され、23文字を超える場合はOSのヒープ上に値が格納されます。詳しくはPat Shaughnessy氏のブログ記事をどうぞ)。
:HEAP_PAGE_OBJ_LIMIT => 409
お次はHEAP_PAGE_OBJ_LIMITです。409は、HEAP_PAGEごとのオブジェクトの最大数です。ところでHEAPやPAGEとは何なのでしょうか。
RubyのHEAPとは、Rubyのオブジェクト空間全体、つまりメモリのことです。HEAPにはすべてのRVALUESが格納されます。HEAPはいくつかのPAGEに分割されます。ガベージコレクタで追加のメモリが必要になった場合、オブジェクトごとに新たなメモリをOSに要求することはありません。もしそうすると、上述のRVALUE_SIZEで学んだようにメモリを40バイト単位で要求することになるので効率が非常に悪くなります。
その代わり、HEAPがさらにメモリを必要とする場合は、まったく新しいPAGEを要求します。1個のPAGEには最大で409個のオブジェクトが含まれています。その理由についてはAaron Patterson氏の素晴らしいブログ記事に書かれています。この定数からもわかるように、1個のPAGEあたりのオブジェクト(つまりRVALUE)の個数の上限は409個に設定されています。
:HEAP_PAGE_BITMAP_SIZE => 56
HEAPとPAGEについてはこれで理解できましたが、今度のBITMAPは何でしょうか。各HEAP_PAGEには、オブジェクトのビットマップ表現も含まれています。各オブジェクトは1つのビットで表現されます。
ところで、:HEAP_PAGE_BITMAP_SIZE => 56は、ビットマップ用にメモリを56バイト使うという意味です。ここで算数のお時間です。1バイトは8ビットなので、56バイトは56 * 8 == 448ビット、つまり448個のオブジェクトを扱えるということなので、上の:HEAP_PAGE_OBJ_LIMIT => 409個のオブジェクトを表現するのに十分な値です。
しかし、そもそもすべてのオブジェクトを表現するのになぜビットマップが必要なのかという疑問が残っています。ビットマップは、Rubyがガベージコレクションに使われる実際のアルゴリズム(具体的には「3色マークアンドスイープ」アルゴリズム」に「統合」されています。このアルゴリズムを詳しく説明しようとするだけで優にブログ記事1本分になるでしょう。さしあたっては、ビットマップがページごとに存在することと、ページ内の各オブジェクトを格納するのに十分なスペースが確保することが重要であるとお考えください。
:HEAP_PAGE_BITMAP_PLANES => 4
正直申し上げると、この定数は謎です。Rubyのソースコードを検索してみると値が4に設定されていることはわかったのですが、変数自体がまったく使われていないのです。この値は私の知る限り未使用です。planeをググるはずがついつい飛行機(airplane)をググって最近ご無沙汰の旅行気分にしばし浸ってしまいました。
冗談はともかく、HEAP_PAGE_BITMAP_PLANESの意味についてご存じの方がいらっしゃったら、ぜひ教えてください。知りたくてたまりません。いにしえのRubyの名残りか何かだと勘ぐっているのですが...
訳注
著者のツイートへの返信を見つけたので貼っておきます。なお、HEAP_PAGE_BITMAP_PLANESを削除するプルリク#4154を著者が出していますが、現時点(2021/06/04)でオープンのままです。
Thanks for sharing! I put up a PR to remove it yesterday https://t.co/yCs5yabfbR so we'll see where that goes
— Jemma Issroff (@JemmaIssroff) February 5, 2021
:HEAP_PAGE_SIZE => 16384
最後の定数はページそのもののサイズです。16384バイトということは、1ページあたり16KB強ということです。これらのページにあるのはは、若干の情報を含むヘッダーと、すべてのRVALUEです。RVALUEを格納する場所は「スロット(slot)」と呼ばれます。先ほど HEAP_PAGE_OBJECT_LIMITは409個と書きましたので、言い方を変えれば1ページあたり最大で409個のスロットがあるということになります。
再び算数のお時間です。1つのページはすべてのRVALUEを十分格納できるはずであり、それに加えてヘッダー情報のスペースも若干使われます。1ページあたりのRVALUEが最大409個であり、1つのRVALUEが40バイトであることがわかったので、409 * 40 == 16,306バイトとなって16384バイトを下回っています。
まとめ
図で理解したい方のために、上を図で解説しました。

ご紹介した定義も手短にまとめました。
- Heap(可変サイズ)
- Rubyのオブジェクトスペース(つまりメモリ)。Rubyはページをすべてここに保存する。
- Page(〜16KB)
- Rubyによるヒープの分割単位。ヒープには複数のページが含まれる。ページ全体で最大409個のスロットが含まれる。
- Slot(40バイト)
- 1つのページ上の領域。1個の
RVALUEがここに保存される。 - RVALUE(40バイト)
- RubyオブジェクトのC表現。オブジェクトの値が含まれることもあれば、OSヒープ上のオブジェクトを指すポインタを含むこともある。
今回の記事は以上です。初めに述べたように、今後もガベージコレクションについてブログ記事と本の両方でいろいろ書いていく予定です。新着記事を見逃したくない方は、原文末尾のフォームでメールアドレスをお知らせいただければ新着記事を通知いたします。
概要
原著者の許諾を得て翻訳・公開いたします。