2010年3月3日水曜日

JRubyの起動時間についてのヒント

原文: チャールズ=オリバー=ナター

JRubyはRuby言語の実装の中では一際起動が遅い事で悪名高くなってしまいました。一部は、JITの生成品がなくてブートストラップして動き出すまでに時間がかかってしまうJVMを使っているせいです。起動の際のボトルネックが報告されて原因が解明されればそれを除く努力を我々はしていますが、JRuby自体にも問題があります。起動の問題は、実は簡単な設定問題である事もしばしばです。JRubyの起動を早くするコツを幾つか見ていきましょう。

注意。JRuby自体の起動は、他のJVM言語に較べると結構いい線いっていますが、MatzのRuby(一般的には最も速い実装とは言えない)の起動は素晴らしく早いです。

クライアント用のJVMを使う


これが何と言っても一番手っ取り早い方法です。OpenJDKとSunのJDKを動かしているJVMであるHotSpotには、「クライアント用」と「サーバ用」の二種類のバックエンドがあります。「クライアント用」はバイトコードをコンパイルする時、最適化は最小限に、始めのうちにササッとやってしまうだけです。一方、「サーバ用」は大規模にしかも全体的に最適化をする訳ですが、これが出来るようになる時点に到着するには長い時間が掛かりますし、最適化の為には多くのリソースが必要です。

ごく最近まではJVMのインストールの多くは、この「クライアント用」を好む傾向にありましたので、JVMの起動時間は早かったのです。しかしながら、多くのオペレーティングシステムに最近使われているJVMに於いてデフォルトで使われるのは「サーバ用」か、或いは(「サーバ用」しかない)64ビットのJVMです。これによって、あなたもJRubyも何もしなくても起動時間が悪くなってしまいました。

以下に例を挙げます。jruby -vを「サーバ用」のJVMで起動させ、RubyGemsをrequire(RubyGemsを多く使うアプリケーションで一番長く掛かる部分です)してみます。

~/projects/jruby ➔ jruby -v
jruby 1.5.0.dev (ruby 1.8.7 patchlevel 174) (2010-03-01 6857a4e) (Java HotSpot(TM) 64-Bit Server VM 1.6.0_17) [x86_64-java]

~/projects/jruby ➔ time jruby -e "require 'rubygems'; require 'active_support'"

real 0m5.174s
user 0m7.643s
sys 0m0.422s

~/projects/jruby ➔ time jruby -e "require 'rubygems'; require 'active_support'"

real 0m5.068s
user 0m7.662s
sys 0m0.449s


なんと、RubyGemsとActiveSupportをロードするのに5秒以上も掛かってしまいました。この場合にはJVMが「64ビット、サーバ用」のJVMが使われています。このバックエンドはランタイムの性能はいいのですが、起動は酷いものがあります。-vのフラグを出した時にこのような結果が出るのならば、普通32ビットの「クライアント用」のJVMを「-d32」と言うフラグを使って走らせる事が出来ます。このようにします。

~/projects/jruby ➔ export JAVA_OPTS="-d32"

~/projects/jruby ➔ time jruby -e "require 'rubygems'; require 'active_support'"

real 0m2.320s
user 0m2.583s
sys 0m0.207s

~/projects/jruby ➔ time jruby -e "require 'rubygems'; require 'active_support'"

real 0m2.275s
user 0m2.580s
sys 0m0.207s


私の手元の、OS X 10.6を載せたMacBook Proでは、「クライアント用」JVMに切り替える事で50%を優に超える向上が観測出来ました。「-client」も試してみて下さい。32ビットのコンピュータでは「サーバ用」を優先する場合も有り得ますので、これと「-d32」とを組み合わせる必要があるかも知れません。色々試してみて、あなたの環境にあったフラグ組み合わせを見つけたら、プラットフォーム、-vの出力結果、そしてどれ程の効果があったかを報せて下さい。

JVMの共用アーカイブを再生成する



Java 5以降のHotSpot JVMではクラスデータシェアリング(CDS)というフィーチャが使えます。元々アップル社によってMac OS X用に開発されたこのフィーチャにより、一般的試用されるJDKのクラスを一つのアーカイブに集め共用メモリの場所にロードします。一度これを使うと、後からJVMを使った場合には、同じデータをリロードせずに、この読み込み専用の共用メモリを使う訳です。これが、ウィンドウズやMac OS XでJVMの起動時間が近年大幅に改善された大きな理由の一つです。この二つのオペレーティングシステムを使っている人はこのヒントは無視してもいいでしょう。

しかしながら、インストーラが、アーカイブを最終的に試用する場所に解凍するだけのLinuxでは共用アーカイブが生成される事はまず無いと考えていいと思います。システムに、この共用アーカイブを再生成させるには、以下のコマンドと同等のものを走らせて下さい。


headius@headius-desktop:~/jruby$ sudo java -Xshare:dump
Loading classes to share ... done.
Rewriting and unlinking classes ... done.
Calculating hash values for String objects .. done.
Calculating fingerprints ... done.
Removing unshareable information ... done.
Moving pre-ordered read-only objects to shared space at 0x94030000 ... done.
Moving read-only objects to shared space at 0x9444bef8 ... done.
Moving common symbols to shared space at 0x9444d870 ... done.
Moving remaining symbols to shared space at 0x945154e0 ... done.
Moving string char arrays to shared space at 0x94516118 ... done.
Moving additional symbols to shared space at 0x945ac5b0 ... done.
Read-only space ends at 0x94612560, 6169952 bytes.
Moving pre-ordered read-write objects to shared space at 0x94830000 ... done.
Moving read-write objects to shared space at 0x94ea64a0 ... done.
Moving String objects to shared space at 0x94ee3708 ... done.
Read-write space ends at 0x94f27448, 7304264 bytes.
Updating references to shared objects ... done.


多くのユーザにとって、JVMのクラスの多くがメモリに残る為、これで起動時間に大きな差が出る事が有ります。「-Xshare」は「-d32」または「-client」と一緒に使ってみる必要が有るかも知れません。他の「-Xshare」オプションなども試してみて下さい。

JRubyのJITのスタートを遅らせるか、無効にする



JRubyのJITコンパイラの興味深い副作用の一つは、動いている時間が短い時など、場合によっては、アプリケーションの実行を遅くさせてしまう事が有るという事です。コンパイラを使うのは勿論無料では有りませんし、コンパイルした結果として出来たバイトコードをロード、チェック、リンクするのにも何らかのコストが掛かります。多くのコードを使うにも関わらず短いコマンドの場合には、JITを無効にするか、そのスタートを遅らせるのを試してみて下さい。



これで見込める効果には限りがありますが、気が狂いそうになった時はやってみて下さい。

他のRubyプロセスの呼び出しを避ける


Kernel#system、Kernel#exec、及びバッククォートを用いて他のRubyプロセスを呼び出すのはRubyist一般によく見られるパターンです。例えば、テストを実行するのに綺麗な環境を用意する必要がある場合などは頷けるのですが、このようなプロセスの呼び出しはランタイム全体に大きな影響を及ぼします。

JRubyが#system、#exec、或いはバッククォートに出くわすと、まず最初に今現在走っているJVMを使って処理を行おうとします。JRubyは昔から(複数のRuby環境が同じプロセスないで動くという)所謂「マルチVM」による実行をサポートしていましたので、こうする事でサブプロセスの呼び出しを素早く行う事が出来ます。実際、Nailgun(これについては後にもっと詳しく触れます)はこれの応用によって複数のJRubyのコマンド実行環境を隔離しているのです。これでパフォーマンスの向上は見込めますが、それでも尚そのようなJRubyプロセスは新規の綺麗なクラスとランタイムを必要としますので、起動には時間が掛かります。

最悪の場合、(例えばsystem 'ruby -e blah > /dev/null'の様に)シェルのリダイレクトを含むなどするとJRubyを同じJVMで起動させる事が出来ません。この場合、JRubyは全く新しくJVMを起動せざるを得ず、JRubyの起動時間というコストを、もう一度全部払う羽目になってしまいます。

出来る事ならば、このようにRubyを呼び出すのを避けるか、Nailgunの様なツールを使うか、specサーバ等を使って同じプロセスを何度も何度も再利用するのが良いでしょう。

起動時の処理をなるべく少なくする



自分で書かなかったコードが起動時に色々とやってしまう場合が多い(凡その場合それはRubyGemsそのものです)でしょうから、このヒントはあまり参考にならないかも知れません。JRubyが直面する悲しい事実の一つはJVMの上での実装であるという事、そしてJVMのエンジンが温まるまでに時間が掛かり、開始当初のコードは後からのコードに較べて遅いという事です。加えて、JRubyが、RubyのコードをJITコンパイルしてバイトコードにするまでには件のコードを何度か実行するという事を考慮すると、JRubyは立ち上がりがあまり良くないという事が解るでしょう。

不可避なものを後回しにしようとしているように見えるかも知れませんが、起動時にあまり何もしない事で思いがけず良い結果が得られる場合もあります。ウィンドウが表示されるまで、或いはサーバの準備が出来るまで、重い処理を無くす事が出来れば、立ち上がりには付き物のパフォーマンス低下から逃れる(またはその低下を分散させる)ことが出来るかも知れません。ディスクに置いたキャッシュを上手く使とか、殆ど読み込むだけのデータはキャッシュとしてセーブしておいて起動時にはこれをリロード、再処理する、といったように、立ち上がりのアルゴリズムの改良も効果的です。

Nailgunを試す



JRuby 1.3ではNailgunのサポートを実装しました。Nailgunは、一つのJVMを複数回に渡る呼び出しに再利用出来るようにする小さなライブラリとクライアント用ツールです。これを使うと、ちょっとしたJRubyのコマンドの起動は何十倍も早くなるかも知れません。詳細はこの記事を読んで下さい。

Nailgunは魔法の弾丸のように見えるかも知れませんが、残念な事に、RubyGemsの起動、或いはRailsアプリケーションの立ち上がり等、或る種のよく見受けられる使用例ではあまり効果がありません。Rubyプロセスを何度も呼び出しするような場合にも効果は期待出来ません。いずれにしても、試してみて、効果があったかどうか報せて下さい。

JVMのフラグを色々試してみる



JVMには起動時間の短縮(或いは延長)に役立つフラグが数多くあります。試してみるべきフラグのリストは私の書いたこの記事を、ヒープのサイズとガーベージコレクターに影響を及ぼすフラグに特に注意して、読んで下さい。特別に効果のあった組み合わせが見つかったら、是非報告をお願いします。

ボトルネックの発見に協力をお願いします



JRubyの起動時間の短縮には、あなたのようなユーザからの起動時間についての調査が大きく貢献しています。ちょっと弄くってみて、或るライブラリのロードに法外な時間が掛かる(又は起動時に膨大な処理をする)と判明した場合、(ハードディスクがスラッシングする、メモリバスが飽和する等)CPU以外の何かがネックになって起動が遅いのならば、JRubyや、ロードしようとしているライブラリのコードに何か改良の余地が有るのかも知れません。ちょっと掘り下げてみて下さい。思わぬ発見が有るかも知れません。

調査の際に役立ちそうなフラグはこういったものが有ります。
  • --sampleはJVMのサンプリングプロファイラーをオンにします。特に正確ではありませんが、桁外れのボトルネックとなるようなものはリストの上に出てくる筈です。
  • -J-Xrunhprorf:cpu=timesはJVMのhprofプロファイラーをオンにして、結果をjava.hprof.txtにセーブします。コードの実行は格段と遅くなりますが、JRubyやJDKのコードの低いレベルでのタイミングをより正確に知ることが出来ます。
  • -J-Djruby.debug.loadService.timing=trueは、起動時間の大部分を占める、requireに掛かった時間を、程々の精度で書き出してくれます。
  • ウィンドウズではtimeコマンドに当たるものがありませんが、-bをJRubyに渡す事で('jruby -b …'といった具合です)これと同様の結果を(JVMの起動に掛かった時間を除いて)得る事が出来ます。

私たちがお手伝いいたします



時として、JRubyにある明らかな設定ミスやバグによってアプリケーションの起動が遅くなってしまう事があります。起動時間がここに挙げて或る例と余りに違いすぎるのは、あなたが使っている環境に問題が有る可能性が大です。そんな場合にはJRubyのメーリングリストに投稿するか、IRCに参加するかして下さい。私たちが、他のJRubyユーザと共に、お手伝いします。

2 件のコメント:

  1. 一箱の→人はこの

    あと、shared archive再生成コマンドの対話部分が抜けているようです。

    返信削除
  2. 遅ればせながら…直しました。

    返信削除