こんにちは、ドリコムのサーバサイドエンジニアの hayabusa333 (橘田)です。
Tech Inside DrecomではElixirの記事を書いております。
こちらもよろしかったらご覧ください。

Elixir Conf Japan 2017 参加レポート vol.1 #elixirjp
Elixir環境構築にて暗号化でエラーにならないために

さて、今年もRubyKaigiに参加させていただきました。
去年に続きまして、2018年6月2日に行われたRubyKaigi2018の3日目のセッションのレポートをさせていただきます。

Parallel and Thread-Safe Ruby at High-Speed with TruffleRuby

高速で並列に動き、Array や Hash にてスレッドセーフで動くTruffleRubyから既存のCRubyや動的言語のスレッドモデルに関しての流れでの話でした。

Ruby2.0やRuby2.6、TruffleRubyにてファミコンのエミュレータを実行した場合にRuby2.0では30FPSです。
Ruby2.6では40FPS、JITが入ることで55FPSでした。
しかしTruffleRubyでは立ち上がり時点では遅いですが、最終的には200FPSほどの速度を出すことができていました(ゲームとしては良いのかはありますが・・・)
既存のCRuby2.0の32倍ほど早いようです。またTruffleRubyではC拡張を含めMRI完全互換を目指しているとのことです。

これほど爆速に動くのは、ASTレベルの最適化を行っており
・Type propagation
・ループ外し
・定数畳み込み
などを行い、とにかく最適化を進めることによって爆速を実現しているとのことでした。

また言語としてはオブジェクトへの操作の安全性を担保して、あとはプログラマに任せるっという選択を行い、爆速で安全な言語の実装として対応しているようです。

今回のTruffleRubyのお話は、TruffleRuby自体の話ではなくCRubyの内部であたり、Ruby以外の動的言語におけるスレッドモデルを含めた問題点からどのようにTruffleRubyは解決しているのかを話されており、とても素晴らしいKeyNoteセッションでした。

最後に”No need to rewrite applications in other languages for speed”という言葉がスライドに記載されており、TruffleRubyでわかったことはRubyのコードを変更なしに速く実行可能であり、安全に並列実行可能であり、並列処理にスレッドを使うプログラムもプログラミングモデルを書き直す必要がないというとても力強い言葉でした。

TruffleRubyの実装に関しましてはこちらのGithubのリンクで確認できます。
コードの半分以上はRubyで記載されていますので読んでみると楽しいかと思います。

Grow and Shrink – Dynamically Extending the Ruby VM Stack

スレッドごとのスタックで使用するメモリは1MBほどであり、最初から確保されています。たくさんのスレッドを使用するとメモリの使用量が増えていく状態だがスレッドの使用数が少ない時はメモリ量は少なくて済みます。
そのためメモリを最初から1MB確保するのではなく、もっと小さい値で取得し、大きくなってきた場合にさらに拡大して確保するように実装した話についてでした。

メモリ上としてはYARVはcall stackが上から下に、Internal Stackは下から上に伸びていくなど、どのようにメモリ管理が行われているかの説明が行われ、どのように変えていったのかを説明されていました。

変更としてはスタックのスタックオーバーフローが発生した場合に新規に2倍にしたスタックを確保し、既存のスタックをコピーしてから既存のスタックを解放するようにしており、無限にスタックを取得しないように既存で1MBとしているスタックのメモリ量を最大値としているとのことでした。

しかし単純に実装しただけでは既存の実装より11%ほど遅くなってしまいます。

そこで LuaのようにCall Stack側は連結リストにし、Internal Stackは上記と同じ形に修正したことによって7%ほどの速度の低下状態となりました。これでも遅いのは動的確保を行っているため遅いようです。

スレッドを多数精製した時のメモリ使用量に関しては、CRubyのtrunkと比べても40%程度の削減が実行できています。

今回の実装のコードはこちらとなります。

The Method JIT Compiler for Ruby 2.6

JITの現状のステータスはRuby2.6.0-preview2では並列で走らせた場合にはrace conditionの問題でバグがあることは認識しています。なので本番でJITで動かすとやばいので動かさないでくださいね。

Ruby2.5.0のVMの実装を変えていないのでJITを動かさないようにすれば、通常で動くように設計をしています。

エミュレータでは2倍速くなっているがRailsでは遅くなるっという状況があり、数ヶ月調査しているが原因が見つけられません。

遅くなっている原因の仮説は下記です。
・longjmpををしていることによって遅くなっているんじゃないか
・仮説が外れるとオーバーヘッドがあって遅くなるんじゃないか
・JITのコンパイル時にホットスポットのプロファイルをとり、キャンセルが発生してオーバーヘッドになっているんじゃないか?
・JITのコンパイルを別スレッドでやっているから遅くならないっというのがまちがっているんじゃないか
・JITのメソッド自体を呼び出すとオーバーヘッドになっているんじゃないか

上記でも、遅くなることは確認できているものもあるがそれだけではなさそうです。

他に遅くなっているものがあるんじゃないかと思いメソッドが大量にあると遅くなるんじゃないか?と調べていみると、nilを返すメソッドが大量にあるだけで爆裂に遅くなっていて、JITが終わったかと思いました。

CPUの命令キャッシュのL1/L2キャッシュの配置場所の違いによって速度の違いが出ており、どのように直していくかが課題になっています。

Cコンパイラに最適化してもらうためにはインライン化が必要です。

・JITコンパイラが呼び出すメソッドを知らないといけない
・呼び出すメソッドをJITコンパイラが管理している必要がある
・キャッシュがないといけない

インライン化を進めたことによって、現状のプロトタイプでの実装ではC言語で呼び出されたメソッドよりも早い実行速度を記録できたようで、これからがとても楽しみですね。

資料はこちらとなります。

Three Ruby performance projects

Ruby3 では速度を3倍速くするっという話があがっていますが、それを達成するためのRubyのパフォーマンスを上げるための3つの改善点に関してのセッションでした。

・CRubyのfloating point の改善
・CrubyのRTLの更新
・CRubyにLight-weight JITの実装

CRuby の浮動小数点が遅く、それはオブジェクトの内部表現が大きいせいとのことでした。
実装の分岐処理を減らすことにより12%の改善を実施できたとのことらしいです。

またRTLの更新を行うことによって、17%~27%の高速化が実現したようです。

最後にLight-weight JITについてお話されていました。
Light-weight JITはMJITが動かないようなIoT系で速度アップを狙うためのJIT実装っというお話でした。
そのためCRubyやmrubyなどでの使用に向けて実装を進めているようです。

最後に

本年度のRubyKaigiも皆様お疲れ様でした。
またRubyKaigiの運営の皆様、本当にありがとうございました。