«前の日記(2008年03月05日) 最新 次の日記(2008年03月07日)» 編集

Matzにっき


2008年03月06日 [長年日記]

_ 取材

「ワールドビジネスサテライト」の取材。

Rubyの、ということではなく、「クラウドソーシング」について オープンソース経験者という視点から聞きたかったらしい。

が、そもそもクラウドソーシングなんて言葉を知らない私は 聞かれてからあわてて検索したりして。 役に立つんだか、立たないんだか。

で、あわてて調べたクラウドソーシングについては、 一時「オープンソースにすれば「コミュニティ」が手伝ってくれてどんどん発展する」と 「誤解」されていたのと、同じ臭いを感じるが気のせいだろうか。

一応、インセンティブが重要というコメントを付けておいたが、 最終的にどういう扱いになるんだか不安なものである。

_ [Ruby] GCの改善について

あちこちでRuby(MRI)のGCについてけなされている。

まあ、たくさんのリソースをかけているJVMのGCに勝つのは 最初から無理な相談なんだが、とはいえ問題があるのであれば 改善したいのが技術者魂というものだ。

指摘されているRuby GCの「課題」は以下のようなものがある。

  • スループット
  • 停止時間
  • メモリリーク
  • プロセスサイズ
  • copy-on-writeとの相性

具体的に問題が生じているものもあれば 理論的可能性のものもあるが、まあ、問題がないとは言わない。

それぞれについて、もう少し解説した上で、 考えられる対策についても述べよう。

スループット

プログラムの実行時間全体の中で、 GCで消費される時間の比をスループットと呼ぶ。 これはGC全体の性能を意味する。

これを根本的に削減する方法はあまりないのだが、 世代別GCが有効だといわれている。

問題は世代別GCを正しく実装するためには 古い世代から新しい世代への参照を検出する必要があり、 そのためにはオブジェクトの書き換えをフックする「ライトバリアー(write barrier)」を 導入しなければならないこと。

以前のRubyに対して世代別GCを実装した結果では、 このライトバリアーのコストが高くて結果的に性能が低下してしまった。

とはいえ、現在の実装でスループットが悪くて使えないというケースは ほとんど聞いたことがないので、それほど強い動機づけはないかもしれない。

停止時間

通常の実装ではGC中には他の作業を行うことができないので、 リアルタイム性の高い処理中にGCが発生すると 処理が引っかかったような印象を与えることがある。 ここで重要になるのが「停止時間」である。

停止時間には「平均停止時間」と「最大停止時間」があり、 応答性に重要なのは最大停止時間の方。 またハードリアルタイム領域では、ただ単に短いだけでなく 予想可能である(たとえば最大100msで終了するとか)であることが 重要である、らしい。

世代別GCではスキャンするオブジェクトが減るため、 平均停止時間は減少するが、フルGCにはそれなりにコストがかかるため 最大停止時間は減少しない。

とはいえ、リアルタイム性が必要な領域でRubyを使うというケースは (まだ)あまり多くないので、停止時間が問題になることもそんなにないような気がする。

もし本当に問題になることがあるならば、 まずは現在のGCのままスイープフェーズをオンデマンドで行うことで、 GC時間をマーク時間だけに限定でき、停止時間を削減できる。

メモリリーク

これは良く指摘されるのだが、 Rubyは保守的GCを使っているので、 本当は参照されていないはずのオブジェクトが参照されていると見なされて いつまでも解放されない(ので結果的にメモリリークになる)ことが ときどき観測されるのだという。

これは確かに私も見たことがある。 個人的には問題だったことはないけど、 サーバープロセスのような長期間生きるプログラムだと 問題になるかもしれない。

で、そういう問題が起きた時の状態をデバッガで観測した経験からいうと Rubyにおけるリークは保守的GC固有の問題(整数値がアドレスと区別できないのでとりあえず生きていると見なす)というよりも、 Cスタックに参照が残っていて生きていると見なされているようだ。 スタック先頭からスタックボトムまで全領域をルートとして 扱っているのが「無駄な参照」を検出してしまう大きな理由なのだろう。

スキャンするCスタックを減らせばよいのだろうが、 スタックの構造はCPUやOSに依存するので、 移植性が下がることになりかねない。

悩ましい。

可能性としては

  • 基本的に保守的にマークする必要があるのはCで実装されたメソッドが使っているスタックだから、 そこだけを記録してルートにする。面倒だが移植性はありそう
  • greenletなどを 参照にOSごとCPUごとにルートを得る。知らないOSでは現状のまま

がある。時間が取れれば考えてみる価値はありそう。

プロセスサイズ

メモリリークとは別に長時間動き続けるRubyプロセスは肥大化する可能性がある。 これはRubyのメモリアロケータがオブジェクトのための領域をmallocする一方で なかなかfreeしないからである。

現状ではオブジェクトのためにheapと呼ばれる領域を割り当てているが ヒープに存在するオブジェクトがすべて解放された時だけ heap領域をfreeしている。 が、Ruby GCはオブジェクトの移動を行わないため、 現在の割り当て方針ではheapがfreeされる可能性はかなり低い。

現在、10000オブジェクト(2回目以降は前回のサイズの1.8倍)ぶん割り当ててるheapのサイズを もっと小さくすればfreeされるチャンスは増す。 もっともあまりfreeされすぎてもmallocとfreeの輻輳が起きて 効率が悪くなってしまうだろうけど。

あと、heapサイズを小さくすると保守的GCの使うオブジェクト判定のコストが上がってしまう 可能性がある。最近、trunkでオブジェクト判定にbsearchを導入したのは このheapサイズ縮小のための伏線である。

copy-on-writeとの相性

マーク・アンド・スイープGCでは、オブジェクトが生きているかどうかを判定するために 各オブジェクトごとにマークビットを設定している。 現状ではこのマークビットはオブジェクト内部に用意されているのだが、 考えてみるとGCごとにすべてのオブジェクトが(少なくとも1ビットは)書き換えられていることを 意味する。

これはUNIX系OSが備えているCopy-on-writeとすこぶる相性が悪い。 せっかく変更されないデータはプロセス間で共有しようとしているのに、 GCが起きるとオブジェクト領域が全部コピーされてしまう。

これだはfork(子プロセス生成)を多用するプログラムの効率が低下してしまう。 で、これについてはパッチを用意したのだが、 heapサイズ削減と相性が悪いし、それでなくてもGC性能が低下しそうである。 どうしたもんだか。

あと、そろそろ発売される日経Linux 4月号でもGCについて解説している。


«前の日記(2008年03月05日) 最新 次の日記(2008年03月07日)» 編集