Rubyのヒープをビジュアル表示する(翻訳)

概要 原著者の許諾を得て翻訳・公開いたします。 英語記事: Visualizing Your Ruby Heap 原文公開日: 2017/09/27 著者: Aaron Patterson サイト: http://tenderlovemaking.com/ Rubyのヒープをビジュアル表示する(翻訳) 前回の記事では、Rubyのオブジェクトがどのようにメモリ上に展開されるかについて軽く触れました。そのときの情報を元に、今回はRubyヒープのダンプを取ってそのヒープの配置や断片化をビジュアル表示するプログラムを書くことにします。 Rubyオブジェクトのレイアウトをざっと復習 単なる復習: Rubyオブジェクトは固定幅です。つまり、あらゆるRubyオブジェクトのサイズは同一(40バイト)になります。オブジェクトは実際にはmallocで割り当てられるのではなく、ページの内部に配置されます。 1つのRubyプロセスには多数のページが含まれ、1つのページには多数のオブジェクトが含まれます。 このオブジェクトはどのページに属するのか? 多くのオブジェクトが1つのページに割り当てられます。各ページは214バイト(訳注: 16,384バイト)です。複数のRubyオブジェクトは同時に割り当てられるのではなく、GCが1つのページ(アリーナとも呼ばれます)を割り当てます。 ページのサイズは正確な214バイトではありません。あるページを割り当てるとき、OSのメモリページに沿ってページを配置したいので、mallocのトータルサイズは4 KB(OSのページサイズ)の倍数よりやや小さい値にする必要があります。mallocシステムコールには若干オーバーヘッドがあるため、連続するOSページにRubyのページを隙間なく収納できるよう、実際にmallocするサイズを総量から差し引かなければなりません。paddingに使うサイズはsizeof(size_t) * 5なので、1ページの実際のサイズは(2 ^ 14) – (sizeof(size_t) * 5)になります。 各ページには、ページ情報の一部を含むヘッダが1つずつあります。ヘッダのサイズはsizeof(void *)です。 つまり、1つのページに保存できるRubyオブジェクトの最大サイズは((2 ^ 14) – (sizeof(size_t) * 5) – sizeof(void *)) / 40になります。 1ページあたりのオブジェクト数には上限があるため、1つのRubyオブジェクトのアドレスの下位14ビットにビットマスクを適用し(ページサイズは214バイトなので、言い換えると14ビットシフトして1ビット残ります)、オブジェクトが実際に配置されるページを算出します。そのビットマスクは~0 << 14です。 あるRubyオブジェクトのアドレスが0x7fcc6c845108の場合、バイナリをASCIIアートで表すと以下のようになります。 11111111100110001101100100001000101000100001000 ^———- ページ アドレス ——–^- object id ^ 上図の「object id」の部分は、昔ながらのRuby object idではなく、単にそのページ上の個別のオブジェクトを表すビットの一部です。アドレス全体は昔ながらの「object id」と考えられます。 これらの数値をRubyのコードに切り出してみましょう。 require ‘fiddle’ SIZEOF_HEAP_PAGE_HEADER_STRUCT = Fiddle::SIZEOF_VOIDP SIZEOF_RVALUE = 40 HEAP_PAGE_ALIGN_LOG = 14 HEAP_PAGE_ALIGN = 1 << HEAP_PAGE_ALIGN_LOG # 2 ^ 14 HEAP_PAGE_ALIGN_MASK = ~(~0 << HEAP_PAGE_ALIGN_LOG) # ページアドレス取得用マスク REQUIRED_SIZE_BY_MALLOC = Fiddle::SIZEOF_SIZE_T * 5 # mallocで必要なpadding HEAP_PAGE_SIZE = HEAP_PAGE_ALIGN – REQUIRED_SIZE_BY_MALLOC # 実ページサイズ HEAP_PAGE_OBJ_LIMIT = (HEAP_PAGE_SIZE – SIZEOF_HEAP_PAGE_HEADER_STRUCT) / SIZEOF_RVALUE 先ほど触れた部分を改めて説明します。Rubyページは、mallocで隙間なく配置されます。言い換えると、あるRubyページが割り当てられるときのアドレスは214で割ることができ、ページのサイズは214よりごくわずか小さくなります。 それでは、あるオブジェクトアドレスを渡すと、そのオブジェクトが配置されたページのアドレスを返す関数を書いてみましょう。 def page_address_from_object_address object_address object_address & ~HEAP_PAGE_ALIGN_MASK end それでは3つのオブジェクトアドレスのページアドレスを出力してみます。 p page_address_from_object_address(0x7fcc6c8367e8) # => 140515970596864 p page_address_from_object_address(0x7fcc6c836838) # => 140515970596864 p page_address_from_object_address(0x7fcc6c847b88) # => 140515970662400 この出力から、最初の2つのオブジェクトは同じページにあるが、3番目のオブジェクトは別のページにあることがわかります。 このページにオブジェクトはいくつあるか? Rubyオブジェクトもアライン(align)されますが、既存のページの内部でアラインされます。アラインされるのは40バイト目(これはそのオブジェクトのサイズでもあります)。つまり、あらゆるRubyオブジェクトが持つ各アドレスはすべて40で割れることが保証されます(これは、数値のようにヒープに割り当てられないオブジェクトについては真ではありません)。 Rubyオブジェクトは決して(訳注: OSによって)割り当てられず、割り当て済みの1つのページ内部に置かれます。そのページは214に沿ってアラインされますが、214で割れるすべての数が40でも割れるとは限りません。つまり、あるページには他のページよりも多くのオブジェクトが保存される場合があるということです。40でも割れるページには、そうでないオブジェクトより1つ多くオブジェクトが保存されます。 ページアドレスを渡すと、そこに保存できるオブジェクトの数とオブジェクトの場所を算出し、ページの情報を表すオブジェクトを1つ返す関数を書いてみましょう。 Page = Struct.new :address, :obj_start_address, :obj_count def page_info page_address limit = HEAP_PAGE_OBJ_LIMIT # ページあたりの最大オブジェクト数 # ページには情報を持つヘッダーが1つあるので、その分も考慮する obj_start_address = page_address + SIZEOF_HEAP_PAGE_HEADER_STRUCT # オブジェクトの開始アドレスがRubyオブジェクトのサイズで割り切れない場合、 # SIZEOF_RVALUEで割り切れる最初のアドレスを見つけるのに必要な # paddingの算出が必要 if obj_start_address % SIZEOF_RVALUE != 0 delta = SIZEOF_RVALUE … Continue reading Rubyのヒープをビジュアル表示する(翻訳)