Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

TCMallocを使ってRubyのメモリ割り当てをプロファイリングする方法

Rubyでは、メモリの割り当てはどのように行われているのでしょうか?

Rubyは「ページ(page)」と呼ばれる塊単位でメモリを確保し、新しく生成されたオブジェクトはこのページの中に格納されていきます。

しかし、

ページがいっぱいになると、さらなるメモリが必要になります。

そのときRubyは、malloc関数を使ってオペレーティングシステム(OS)に追加のメモリを要求します。

このmalloc関数はOS標準で提供されているものですが、代替となる実装に差し替えることも可能です。

その代表的な実装のひとつが、Google製のTCMallocです。

TCMallocは「gperftools(Google Performance Tools)」スイートの一部として提供されています。

これらのツールを活用すれば、Rubyが実際にどのようにメモリを割り当てているのかを詳細に調査できます。

さらにLinux環境では、LD_PRELOAD環境変数のおかげで、システム標準のmalloc関数をTCMallocへ置き換えて実行できます。

具体的には次のように実行します:

LD_PRELOAD="/usr/lib/libtcmalloc.so" ruby -e "puts 123"

ただしこれはライブラリを読み込んでいるだけで、まだデータ収集自体は有効になっていません。

それでは、有効化の手順を見ていきましょう。

プロファイラを有効化する

TCMallocのプロファイラは、環境変数HEAPPROFILEを追加で指定することで有効化できます。

LD_PRELOAD="/usr/lib/libtcmalloc.so" HEAPPROFILE=/tmp/profile ruby -e "puts 123"

実行すると、次のような出力が得られます。

Starting tracking the heap
123
Dumping heap profile to /tmp/profile.0001.heap (Exiting, 2 MB in use)

まず、プロファイラが有効になったことを示すメッセージが表示されます。

続いて確認できる情報は以下の通りです:

  • プログラム本体の出力結果
  • 出力先のファイル名(profile.0001.heap)
  • プログラムが使用したメモリ量(2 MB)

このファイルの中身を読み取るには、TCMallocに同梱されているもうひとつのツールが必要になります。

pprof --text `which ruby` /tmp/profile.0001.heap | head -10

実行すると、次のような出力が得られます。

Total: 2.4 MB
   1.1  44.7%  44.7%      1.1  44.7% 0x00005570fa4df074
   0.7  27.8%  72.5%      0.7  27.8% 0x00005570fa4e0c09
   0.4  15.3%  87.8%      0.4  15.3% 0x00005570fa4db460
   0.1   5.9%  93.7%      0.1   5.9% 0x00005570fa4df19f
   0.1   3.2%  96.9%      0.1   3.2% 0x00005570fa6349a0
   0.0   1.4%  98.3%      0.0   1.4% 0x00005570fa589924
   0.0   0.3%  98.6%      0.0   0.3% 0x00005570fa59c4f2
   0.0   0.3%  98.8%      0.0   0.3% 0x00005570fa4db48a
   0.0   0.2%  99.0%      0.0   0.2% 0x00005570fa4dbaa5
   0.0   0.2%  99.1%      0.0   0.2% _dl_new_object

しかしこれでは、ただのメモリアドレスの羅列に見えますよね。関数名を確認するためには、デバッグシンボル付きでビルドされたRubyが必要です。

デバッグシンボルがあれば、次のように関数名が表示されます:

Using local file ruby.
Using local file /tmp/profile.0001.heap.
Total: 2.9 MB
   1.0  36.2%  36.2%      1.0  36.2% objspace_xmalloc0
   0.7  26.1%  62.4%      0.7  26.1% aligned_malloc
   0.5  18.8%  81.1%      0.5  18.8% objspace_xcalloc
   0.3   9.9%  91.0%      0.3   9.9% stack_chunk_alloc
   0.1   3.7%  94.7%      0.1   3.7% objspace_xrealloc
   0.1   2.7%  97.4%      0.1   2.7% Init_Method
   0.0   1.3%  98.7%      0.0   1.7% onig_new_with_source
   0.0   0.4%  99.2%      0.8  26.6% heap_page_allocate
   0.0   0.2%  99.4%      0.0   0.2% add_bitset

ここに表示されているのは、「MRI(Rubyインタプリタ)のどの関数が、どれだけのメモリを割り当てたのか」という情報です。

興味深いポイントとして、aligned_mallocはRubyオブジェクト用の新しいページを割り当てるために使われる関数、stack_chunk_allocはGC(ガベージコレクタ)自身がマーキングフェーズ中に利用する関数、そしてobjspace_xmalloc0 / objspace_xcallocは、文字列や配列などRVALUE構造体に収まりきらないデータのための領域を確保するために使われている、ということが分かります。

さて、ここで重要な点があります。

TCMallocはRubyオブジェクトについては何も知りません。TCMallocが行っているのは、malloc・calloc・reallocへの呼び出しを追跡し、どれだけのメモリが要求されたかを記録することだけです。

もしRubyレベルでのヒープダンプを取得したい場合は、ObjectSpace.dump_allを使いましょう。これにより、アプリケーション内のすべての生存オブジェクトとそれぞれのメモリサイズを含んだJSONファイルが得られます。

一方、TCMallocで確認できるのは、最終的にメモリを要求しているC言語レベルの関数群を可視化したものです。

pprof --web `which ruby` /tmp/profile.0001.heap

このコマンドを実行すると、ChromeやFirefoxでSVG形式の可視化グラフが開きます。

TCMallocを使ってRubyのメモリ割り当てをプロファイリングする方法

TCMallocは、このような便利なプロファイリング機能を提供してくれるだけでなく、導入することでアプリケーションのパフォーマンスを4〜9%程度向上させられる可能性もあります。さらに、プロファイラを備えたもうひとつのmalloc実装であるjemallocを試してみるのも良い選択肢です。

まとめ

今回は、gperftools(Google Performance Tools)を活用して、Rubyインタプリタのメモリ使用量を可視化・分析する方法を解説しました。

最後までお読みいただきありがとうございました!🙂

  1. TCMallocを使ってRubyのメモリ割り当てをプロファイリングする方法

    Rubyでは、メモリの割り当てはどのように行われているのでしょうか? Rubyは「ページ(page)」と呼ばれる塊単位でメモリを確保し、新しく生成されたオブジェクトはこのページの中に格納されていきます。 しかし、 ページがいっぱいになると、さらなるメモリが必要になります。 そのときRubyは、malloc関数を使ってオペレーティングシステム(OS)に追加のメモリを要求します。 このmalloc関数はOS標準で提供されているものですが、代替となる実装に差し替えることも可能です。 その代表的な実装のひとつが、Google製のTCMallocです。 TCMallocは「gperftools(Goog

  2. Rubyでパーサーを自作する方法!StringScannerを使った実装手順を徹底解説

    パース(構文解析)とは、文字列の集まりから意味を読み取り、プログラムが扱える形のデータへと変換する技術です。正規表現でも文字列の解析は可能ですが、すべての場面に適しているわけではありません。 たとえば、正規表現でHTMLを解析するのはあまり良い方法ではないというのは、プログラミング界隈ではよく知られた話です。 Rubyにはnokogiriという強力なライブラリがあり、HTMLの解析はこれに任せられます。しかし、自分でパーサーを一から作ってみると、文字列処理や構文解析の仕組みについて多くのことを学べます。それでは早速始めていきましょう! Rubyでのパースの基本:StringScannerクラス