ククログ

株式会社クリアコード > ククログ > Ruby 2.4の新機能:rb_gc_adjust_memory_usage() - バインディングとGCの関係を改善するAPI

Ruby 2.4の新機能:rb_gc_adjust_memory_usage() - バインディングとGCの関係を改善するAPI

(おそらく)2016年のクリスマスにRuby 2.4.0がリリースされます。Ruby 2.4.0で導入されるrb_gc_adjust_memory_usage()というAPIについて紹介します。

バインディングとGCの関係

このAPIはバインディングとGCの関係を改善するためのAPIです。(バインディングについてはRubyKaigi 2016:How to create bindings 2016を参照してください。)

まず、バインディングとGCの関係にどのような問題があったかを説明します。RubyのGCはRubyのオブジェクトがどのくらいメモリーを使っているかを知っているのでメモリーを結構使っているなーと思ったらGCして不要なメモリーを解放したり再利用したりします。これにより、プロセスのメモリー使用量の肥大化を防いでいます。しかし、RubyのGCはバインディング対象のライブラリーがどのくらいメモリーを使っているかを知りません。そのため、バインディング対象のライブラリー内で結構メモリーを使ってもRubyのGCはなかなか走りません。その結果、プロセスのメモリー使用量が肥大化しがちです。マルチメディア系のライブラリー(画像や動画を扱うライブラリー)やインプロセスのデータベースライブラリー(たとえばRroonga)はこうなりがちです。

rb_gc_adjust_memory_usage()はこの問題を解決する手段を提供します。バインディングはrb_gc_adjust_memory_usage()を使うことでバインディング対象のライブラリーがどのくらいメモリーを使っているかをRubyのGCに伝えることができます。これでRubyのGCはメモリー使用量をより正しく知ることができます。その結果、適切なタイミングでGCを実行しやすくなります。

使い方

rb_gc_adjust_memory_usage()の使い方は簡単です。RubyのGCが知らないところでメモリーを確保したらそのサイズを正の値で呼び出し、そのメモリーを解放したらそのサイズを負の値にして呼び出します。なお、RubyのGCに伝えるサイズは確保したサイズと解放したサイズが同じになれば正確でなくても構いません。GCを実行するタイミングの参考に使われるだけなので概算で大丈夫です。

具体的な使用例を見てみましょう。rcairoという画像を扱うライブラリーのバインディングの使用例を見ます。rcairoはrb_gc_adjust_memory_usage()に対応しているバインディングです。

rcairoでは2次元画像を扱うクラスCairo::ImageSurfaceのインスタンスを生成したときに画像のバッファーサイズを確保したメモリーサイズとしてrb_gc_adjust_memory_usage()を呼び出しています。バッファーサイズは1行のバイト数(cairo_image_surface_get_stride(surface))×行数(cairo_image_surface_get_height(surface))で計算できます。ソースはext/cairo/rb_cairo_surface.cです。

cairo_surface_t *surface;
surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32,
                                      NUM2INT (width),
                                      NUM2INT (height));
{
  ssize_t added_memory_usage;
  added_memory_usage =
    cairo_image_surface_get_stride (surface) *
    cairo_image_surface_get_height (surface);
  rb_gc_adjust_memory_usage (added_memory_usage);
}

インスタンスを解放したときは画像のバッファーサイズを負の値にしてrb_gc_adjust_memory_usage()を呼び出します。

{
  ssize_t freed_memory_usage;
  freed_memory_usage =
    -(cairo_image_surface_get_stride (surface) *
      cairo_image_surface_get_height (surface));
  rb_gc_adjust_memory_usage (freed_memory_usage);
}
cairo_surface_destroy (surface)

これだけです。実際は画像のバッファーサイズ+αのメモリーを確保していますが、そこは画像のバッファーサイズに比べれば誤差ですし正確にカウントすることも面倒なので、RubyのGCには伝えていません。

効果

実際にどのくらい効果があるかrcairoのケースを見てみましょう。rcairoは画像を扱うライブラリーのバインディングなのでバインディング対象のライブラリーが大きめのメモリーを確保する傾向にあります。そのため、rb_gc_adjust_memory_usage()が有効なケースです。

次のような600x600のサイズの画像を1000個作るだけのスクリプトを考えます。

require "cairo"

width = 600
height = 600
1000.times do
  Cairo::ImageSurface.new(:argb32, width, height)
end

rb_gc_adjust_memory_usage()を使ってバインディング対象のライブラリーが使っているメモリー使用量をRubyのGCに伝えた場合となにもしない場合のメモリー使用量は次のようになりました。横軸が繰り返し回数で縦軸がメモリー使用量です。青い線がrb_gc_adjust_memory_usage()を使った場合で黄色い線が使っていない場合です。青い線の方は最大メモリー使用量が抑えられていますが、黄色い線の方は最大メモリー使用量が増え続けています。(繰り返し回数が少ないうちは黄色い線の方がメモリー使用量が少ないことは興味深い結果です。)

メモリー使用量

なお、このグラフは次のようなスクリプトでデータを取得しgnuplotで描画しました。gnuplotのスクリプトはgc-triggerで使っているものを少しいじって使いました。

require "cairo"

width = 600
height = 600
1000.times do |i|
  Cairo::ImageSurface.new(:argb32, width, height)
  case File.readlines("/proc/self/status").grep(/\AVmRSS:/)[0]
  when /\AVmRSS:\s+(\d+)\s+kB/
    vm_rss_mib = $1.to_i / 1024.0
  end
  puts [i, GC.count, vm_rss_mib].join("\t")
end

まとめ

Ruby 2.4の新機能であるrb_gc_adjust_memory_usage()を紹介しました。バインディングを作っている人は活用してください。

この機能は東京Ruby会議11で話したことがきっかけでRuby本体に提案した機能です。前からどうにかならないかなぁと問題意識は持っていましたが、よい案が浮かばないこともあり特になにもアクションを起こしていませんでした。話す機会があったことでちゃんと考える機会になり、結果としてRubyがよくなることにつながりました。みなさんも問題意識があることをまとめる機会を作ってみてはどうでしょうか。まとめることで解決案を思いついて改善につながるかもしれません。