Benchmarking CRuby, MJIT, YJIT, JRuby, and TruffleRuby

原文はこちら。
The original article was written by Benoit Daloze (TruffleRuby Lead at Oracle Labs).
https://medium.com/graalvm/benchmarking-cruby-mjit-yjit-jruby-and-truffleruby-6a7178ca6906

このブログでは、最新のRubyベンチマークスイートであるyjit-benchを使い、多くのバージョンのRubyと最新のRuby Just-in-Timeコンパイラ(JIT)のベンチマークを行っています。

yjit-bench : Set of benchmarks for the YJIT CRuby JIT compiler
https://github.com/Shopify/yjit-bench

これらの14のベンチマークでCRuby 3.1と比較した幾何平均の高速化率はMJIT 1.26倍、YJIT 1.39倍、JRuby 1.86倍、TruffleRuby 6.23倍でした。このブログ記事は、Benoit氏のブログにも掲載されています。

Benchmarking CRuby, MJIT, YJIT, JRuby and TruffleRuby
https://eregon.me/blog/2022/01/06/benchmarking-cruby-mjit-yjit-jruby-truffleruby.html

Outline

Approach

Rubies

以下のバージョンのRubyでベンチマークをとっています。

種類バージョン
CRuby 2.0ruby 2.0.0p648 (2015-12-16 revision 53162)
CRuby 2.7ruby 2.7.5p203 (2021-11-24 revision f69aeb8314)
CRuby 3.0ruby 3.0.3p157 (2021-11-24 revision 3fb7d2cadc)
CRuby 3.1ruby 3.1.0p0 (2021-12-25 revision fb4df44d16)
CRuby 3.1+MJITruby 3.1.0p0 (2021-12-25 revision fb4df44d16) +MJIT
CRuby 3.1+YJITruby 3.1.0p0 (2021-12-25 revision fb4df44d16) +YJIT
JRuby+invokedynamic, on the system JDK 11jruby 9.3.2.0 (2.6.8) 2021-12-01 0b8223f905 OpenJDK 64-Bit Server VM 11.0.10+9 on 11.0.10+9 +indy +jit
TruffleRuby JVM EE (JDK 11)truffleruby 22.0.0-dev-9bdb2dfc, like ruby 3.0.2, GraalVM EE JVM

すべてのベンチマークは、2022年1月4日に、AMD Ryzen 7 3700X 8-Core Processor、32GBのメモリとM.2 SSDを搭載したLinuxで実行しました。

ベースライン(1.0倍)として CRuby 3.1を選択しました。これはJIT無しのCRuby(デフォルト)に対してMJITやYJITのゲインがどれほどあるかを簡単に比較するためです。

CRuby 2.0ではベンチマークの一部だけを実行できます。CRuby 3.1にはMJITとYJITの両方が含まれているため、それらを測定するのに便利です。TruffleRubyについては、すでに多くのRuby実装があるため、グラフが見やすくなるように、(Native/JVM CE/EEから)1つのバリエーションを選びました。TruffleRuby は、近日公開予定の GraalVM 22.0 のリリースブランチから使用しました。

yjit-bench

非常にノイズが多く、いくつかのRubyバージョンで動作しないため、コミット1751916cebのyjit-bench benchmark suiteから、jekyllベンチマークを除く14のマクロベンチマークをすべて実行しました。マイクロベンチマーク()は、一般的なRubyの性能を比較するためというより、JITを調整するためにあるように思えるので、報告の対象外にしています。もちろん、マイクロベンチマークは実際のワークロードに近いものを表すものではありません。面白いことに、TruffleRubyではこれらのマイクロベンチマークの半分で最適化(1000倍以上高速化)されています。

yjit-bench benchmark suiteにはさまざまなベンチマークが含まれていますが、主に Shopify の Noah Gibbs (Ruby 3×3 のベンチマークも担当) と Maxime Chevalier-Boisvert (YJIT のリーダー) がメンテナンスしています。このbenchmark suiteの大きな利点は、すべてのベンチマークの実行が実に簡単で、(bundle installdb:migrateなどを含む)セットアップは自動的に行われ、ベンチマークの実行は

ruby -I./harness benchmarks/some/benchmark.rb

だけです。サブプロセスもマジックもないので、実行もプロファイリングも本当に簡単です。

Warmup

関連するメソッドがコンパイルされた後のピーク性能、つまりウォームアップを十分に行った後の性能を測定しています。これは一般的にJITの比較に使われるものであり、このベンチマーク群が測定しようとするものでもあります。すべてのベンチマークは十分なウォームアップを伴って実行されました。ウォームアップ(の完了)は、これまでのすべての反復処理における中央絶対偏差を追跡し、中央絶対偏差が閾値に達するまで反復処理を継続するカスタムハーネスを使用して自動的に検出します(詳細はこちら)。

カスタムハーネス
https://github.com/eregon/yjit-bench/blob/harness-warmup-20220104/harness-warmup/harness.rb

一般に、より高度なJITにはより多くのウォームアップが必要です。TruffleRubyとGraalVMチームは、多層コンパイルの使用、JITコンパイル済みコードの永続化、インタプリタ性能の最適化などのアイデアで、この問題に取り組んでいます。

Multi-Tier Compilation in GraalVM
https://medium.com/graalvm/multi-tier-compilation-in-graalvm-5fbc65f92402
https://logico-jp.io/2021/04/25/multi-tier-compilation-in-graalvm/
Truffle AOT Overview
https://www.graalvm.org/graalvm-as-a-platform/language-implementation-framework/AOTOverview/
Open Postdoc Position on Language Implementation and Concurrency
https://twitter.com/smarr/status/1383076749043511300
https://stefan-marr.de/2021/02/open-postdoc-position-on-language-implementation-and-concurrency/

グラフには、すべての反復実行の後半について、中央値を棒の高さ、中央絶対偏差をエラーバーとしてプロットしています。前半はウォームアップと呼ばれ、通常はノイズが多く出ます。これらの推定値は、通常の平均値や標準偏差とは異なり、ロバストである(つまり、ある反復におけるGCのような外れ値に影響されない)ことから選択しました。ご覧の通り、ほとんどすべてのベンチマークでエラーバーが非常に小さくなっています。これは、ウォームアップが十分で、安定した性能に到達したことを示しています。

medianはすべての値をソートした場合の中央値です。中央絶対偏差 (median absolute deviation) は全絶対偏差の中央値であり、これはある値と中央値の間の差の絶対値を表します。換言すると、中央値は真ん中の値(通常はほとんどの値に最も近い値)であり、中央絶対偏差は中央値からどれだけ離れているかを示すものです。

Benchmarks

activerecordベンチマークでは、RailsのActiveRecordを使ってデータベースに問い合わせ、最初のPostのタイトルを返します。

activerecordベンチマーク
https://github.com/Shopify/yjit-bench/blob/1751916ceb/benchmarks/activerecord/benchmark.rb

便宜上、インメモリデータベースのsqlite3を使用しています(sqlite3はC言語の拡張gemです)。JRubyでは、代わりにactiverecord-jdbcsqlite3-adapterを使っています。
このベンチマークはデータベースの性能で左右されると思われるかもしれませんが、明らかにそうではないようで、YJITはCRuby 3.1の1.42倍、TruffleRubyは4.54倍のスピードアップを示しました。ActiveRecordやsqlite3 gemにはかなりのRubyロジックがあり、Ruby JITはこれらを最適化できる可能性があります。また、TruffleRubyはC言語拡張をRubyコードと一緒にJITコンパイルしているので、C言語拡張の実行速度も速くなる可能性があります。

binarytreesベンチマーク(Computer Language Benchmarks Gameより。以後CLBGと略します)では、多数の再帰呼び出しをしてバイナリツリーを作成します。

binarytreeベンチマーク
https://github.com/Shopify/yjit-bench/blob/1751916ceb/benchmarks/binarytrees/benchmark.rb
Computer Language Benchmarks Game
https://benchmarksgame-team.pages.debian.net/benchmarksgame/description/binarytrees.html#binarytrees

このベンチマークは、多くのアロケーションを行うためGCに負荷がかかり、1つのツリーをしばらく維持することになります。ここでは、MJITが1.79倍、JRubyが2.06倍、TruffleRubyが2.73倍でした。

erubiベンチマークは、Railsのデフォルトテンプレートエンジンであるerubi gemを使ってERBテンプレート(322個のgemを持つgem serverのインデックス)をレンダリングします。

erubiベンチマーク
https://github.com/Shopify/yjit-bench/blob/1751916ceb/benchmarks/erubi/benchmark.rb
ERBテンプレート
https://github.com/Shopify/yjit-bench/blob/1751916ceb/benchmarks/erubi/simple_template.erb

レンダリングされたテンプレートの長さは166563バイトです。TruffleRubyはStringの連結が非常に効率的であるため、この処理で素晴らしいパフォーマンスを発揮し、5.26倍のスピードアップを達成しました。

Specializing Ropes for Ruby
https://eregon.me/blog/assets/research/specializing-ropes-for-ruby.pdf

YJITは、現在このベンチマークで使用されているバイトコード命令をサポートしていないためうまく最適化できていません。

Implement opt_aref_with #297
https://github.com/Shopify/yjit/issues/297

erubi_railsベンチマークは、Discourseから送られてくるERBビューをレンダリングする小さなRailsアプリケーションです。レンダリングされるテンプレートの長さは9369バイトです。

erubi_railsベンチマーク
https://github.com/Shopify/yjit-bench/blob/1751916ceb/benchmarks/erubi_rails/benchmark.rb
Discourse
https://www.discourse.org/
ERB view
https://github.com/Shopify/yjit-bench/blob/1751916ceb/benchmarks/erubi_rails/app/views/fake_discourse/topics_show.html.erb

このベンチマークでは、トピックのページを1回の繰り返しで100回レンダリングします(ネットワークは介在しません)。このベンチマークでは、Railsのコンテキストでerubiに焦点を当てているため、データベースは関与しません。YJITは1.31倍、JRubyは1.82倍、TruffleRubyは3.3倍のスピードアップを達成しました。

CLBGのfannkuchreduxベンチマークは、広範囲に渡って配列の作成と操作を行います。

fannkuchreduxベンチマーク
https://github.com/Shopify/yjit-bench/blob/1751916ceb/benchmarks/fannkuchredux/benchmark.rb
fannkuch-redux description
https://benchmarksgame-team.pages.debian.net/benchmarksgame/description/fannkuchredux.html#fannkuchredux

MJITとYJITは、このベンチマークでは高速化を達成できていないようです。おそらく、このベンチマークはArray操作に重点を置いており、Array操作はCRubyではCで定義されているので、MJIT/YJITは最適化できないからです(単純なa[i]a[i] = vは例外ですが、このベンチマークではインタープリターよりも最適化できていないようです)。一方、JRubyとTruffleRubyはArray演算の最適化とJITコンパイルが可能で、TruffleRubyではArray storage strategyを使用しています。

Array Storage Strategies (32枚目)
https://speakerdeck.com/eregon/parallel-and-thread-safe-ruby-at-high-speed-with-truffleruby?slide=62

JRubyでは5.53倍、TruffleRubyでは33.88倍高速化しました。

hexapdfベンチマークはhexapdf gemを使い、長いテキスト(The Odyssey)を行幅50文字にラップしてPDFファイルにレンダリングし、/tmpに保存します。

hexapdfベンチマーク
https://github.com/Shopify/yjit-bench/blob/1751916ceb/benchmarks/hexapdf/benchmark.rb
The Odyssey
https://gutenberg.org/ebooks/1727

hexapdfはすべてRubyで書かれています(C拡張は使っていません)。テキストの折り返しは算術演算に大きく依存するため、JITがその点で良い仕事をすることが多々あります。YJITは1.4倍、TruffleRubyは4.38倍(ばらつきあり)高速化されました。CRuby 3.1 MJITは buffer error (Zlib::BufError) でこのベンチマークに失敗しました。

Bug 18277 buffer error (Zlib::BufError) in Zlib::Deflate#deflate when using MJIT
https://bugs.ruby-lang.org/issues/18277

leeベンチマークは、Chris Seaton(彼のブログ記事を参照)によるもので、彼の博士課程での研究から生まれたものです。

leeベンチマーク
https://github.com/Shopify/yjit-bench/blob/1751916ceb/benchmarks/lee/benchmark.rb
Context on STM in Ruby
https://chrisseaton.com/truffleruby/ruby-stm/

このベンチマークは、回路基板上の配線を交差しないように並べるもので、これはシーケンシャルなパターンのベンチマークです。このアプローチ自体は科学的な文献から得たもので、Chrisは単純なRubyだけで書きました。特にどのRuby実装に対しても最適化していないことは注目に値すると思われます。彼のブログでは、TruffleRubyはCRuby 2.7よりも10倍以上高速であることが示されています。最新のTruffleRubyでは、22倍ものスピードアップを実現しています。

liquid-render ベンチマークは、ベンチマーク開始前にパースされたすべてのパフォーマンステスト用 Liquid テンプレートをレンダリングします。

liquid-renderベンチマーク
https://github.com/Shopify/yjit-bench/blob/1751916ceb/benchmarks/liquid-render/benchmark.rb
パフォーマンステスト用Liquidテンプレート
https://github.com/Shopify/yjit-bench/tree/1751916ceb/benchmarks/liquid-render/performance/tests

ご存知のように、Shopify では Liquid テンプレートを素早くレンダリングすることが重要です。

How to Debug Liquid Render Performance with Shopify Theme Inspector for Chrome
https://www.shopify.com/partners/blog/shopify-liquid-debug

Liquid は典型的なテンプレートエンジンではなく、Ruby コードを生成して eval を実行し、(ERB エンジンのように)生成されたメソッドを呼び出すということはしません。代わりに、Liquid はテンプレートを表す AST を解釈し、セキュリティ上の理由と eval 実行を回避しています。

Liquid
https://github.com/Shopify/liquid/blob/db3999a008/README.md#introduction

YJIT は 1.61 倍、TruffleRuby は 4.86 倍のスピードアップを達成しましたが、これは TruffleRuby の高度な文字列表現 (Ropes) によるものと、JITコンパイルにより Liquid インタプリタがより良くなったことによるものです。

Specializing Ropes for Ruby
https://eregon.me/blog/assets/research/specializing-ropes-for-ruby.pdf

mailベンチマークはmail gem(詳細は以下のURLを参照)を使ってメールをパースします。

mailベンチマーク
https://github.com/Shopify/yjit-bench/blob/1751916ceb/benchmarks/mail/benchmark.rb
Add benchmark of mail gem parsing #40
https://github.com/Shopify/yjit-bench/pull/40

一部のパースはRagelが生成したパーサーが実行します。

Ragel State Machine Compiler
http://www.colm.net/open-source/ragel/

YJITとTruffleRubyで高速化されています。

CLBG由来のnbodyベンチマークは、太陽の周りの木星、土星、天王星、海王星の軌道をモデル化したものです。

nbodyベンチマーク
https://github.com/Shopify/yjit-bench/blob/1751916ceb/benchmarks/nbody/benchmark.rb
n-body description
https://benchmarksgame-team.pages.debian.net/benchmarksgame/description/nbody.html#nbody

このベンチマークは主に算術計算とインスタンス変数へのアクセスで構成されています(惑星をオブジェクトとして表現しています)。JRubyは4.43倍という良好な高速化を達成しました。TruffleRubyはCRuby 3.1の約50倍の速度で、これは単純にすごいことです。

optcarrotベンチマークは、Yusuke Endohが作成したRuby 3×3の有名なベンチマークの一つです(私のブログ記事です)。

Optcarrot: A NES Emulator for Ruby Benchmark
https://github.com/Shopify/yjit-bench/blob/1751916ceb5de60311b6c632caed34f68d2a7982/benchmarks/optcarrot/README.md
https://github.com/Shopify/yjit-bench/blob/1751916ceb/benchmarks/optcarrot/benchmark.rb
Yusuke Endoh (@mametter)
https://twitter.com/mametter
Running Optcarrot, a Ruby NES emulator, at 150 fps with the GUI!
https://eregon.me/blog/2016/11/28/optcarrot.html

これはファミコンのエミュレータで、このベンチマークではLan Masterゲームを1回の反復で200フレーム描画します。MJITはこのベンチマークでCRuby 2.0の3倍程度高速になることがわかっています。

Ruby 3.0.0 Released
https://www.ruby-lang.org/en/news/2020/12/25/ruby-3-0-0-released/

今回の実行では、3.1MJITはCRuby 2.0のちょうど3.0倍高速でした。TruffleRubyはこのベンチマークが出たときから得意なことがわかっていて、今回の計測ではCRuby 2.0に対して10.4倍高速でした。

psych-load ベンチマークは Psych.loadYAML.load と同じ)を使って一連のYAMLファイルをパースします。

psych-loadベンチマーク
https://github.com/Shopify/yjit-bench/blob/1751916ceb/benchmarks/psych-load/benchmark.rb
このベンチマークでパースする対象のYAMLファイル
https://github.com/Shopify/yjit-bench/tree/1751916ceb/benchmarks/psych-load/yaml

CRuby 2.0ではPsych 4.0.1ではなくPsych 2.2.4が使用されていますが、これはPsych 2.0互換の最新版であるためです。ベンチマークのかなりの部分は、psych C拡張(libyaml YAMLパーサーを使用)内で費やされていると思われますが、MJITとYJITがここでいくつかのスピードアップを示すように、C拡張(CRuby JITが役に立たない)だけでなくRubyコードでも時間を費やしていることが分かります。

railsbenchベンチマークは、headiusのpgrailsbenchをベースにしたk0kubunのrailsbenchからヒントを得たものです。

railsbenchベンチマーク
https://github.com/Shopify/yjit-bench/blob/1751916ceb/benchmarks/railsbench/README.md
https://github.com/Shopify/yjit-bench/blob/1751916ceb/benchmarks/railsbench/benchmark.rb
railsbench
https://github.com/k0kubun/railsbench
pgrailsbench
https://github.com/headius/pgrailsbench

これは、Postモデルで骨格がつくられた小さなブログ風Railsアプリです。この亜種では、便宜上データベースにsqlite3を使用し(ディスクに保存)、すべてを単一プロセスで実行しています(実行はずっと単純です)。ベンチマークは、投稿のインデックス (HTML と JSON の両方) と、100 件の投稿のそれぞれを、固定シードで疑似的にランダムな順序で繰り返し訪問します。Railsが生成するデフォルトのERBテンプレートを使用しています。MJITは1.09倍(MJITがRailsを高速化するのに時間がかかりました)、YJITは1.33倍、JRubyは1.13倍、TruffleRubyは3.27倍の高速化が得られています。

Ruby 3 JIT can make Rails faster
https://k0kubun.medium.com/ruby-3-jit-can-make-rails-faster-756310f235a

つまり、この小さなRailsベンチマークでは、TruffleRubyの応答時間はCRuby 3.1の3倍以上も速いのです。

Tobias Pfeifferが作成したRubykonを使うrubykonベンチマークは、その作者によるブログ記事でよく知られています。

rubykonベンチマーク
https://github.com/Shopify/yjit-bench/blob/1751916ceb/benchmarks/rubykon/benchmark.rb
Rubykon – A Go-Bot written in Ruby. Work not so much in progress anymore but also not quite done
https://github.com/PragTob/rubykon
The great Rubykon Benchmark 2020: CRuby vs JRuby vs TruffleRuby
https://pragtob.wordpress.com/2020/08/24/the-great-rubykon-benchmark-2020-cruby-vs-jruby-vs-truffleruby/
Benchmarking a Go AI in Ruby: CRuby vs. Rubinius vs. JRuby vs. Truffle – a year later
https://pragtob.wordpress.com/2017/01/24/benchmarking-a-go-ai-in-ruby-cruby-vs-rubinius-vs-jruby-vs-truffle-a-year-later/

rubykonは囲碁用のAIです。YJITは2.15倍、JRubyは2.59倍のスピードアップを達成しました。TruffleRubyはそれを上回る9.54倍の高速化を達成しています。

Summary

今回分析したベンチマークは、様々なサイズで様々な特徴を持っており、現実的なものとそうでないものがあります。ベンチマーク結果だけを幾何平均しても、その多様性を考えると、あまり良いまとめにはならないのではないかと思いますが、とはいえこれらのベンチマークについて、1つの数値でまとめられるのは良いことです。なお、CRuby 2.0は14個中7個しか実行できず、MJITは14個中13個しか実行できません(hexapdfは不可)。他のRuby版では14個すべてのベンチマークが実行できます。それゆえ、このグラフではエラーバーは意味を持ちません。

このグラフは左から右に行くほど高くなるのが納得できるでしょう。これらのベンチマークの幾何平均 (geo mean) を調べた結果、以下のようになりました。

  • 古いバージョンよりも新しいバージョンのCRubyが速い
  • MJITとYJITの有無ではMJITとYJITがあるほうが速い
  • JRubyの幾何平均はMJITとYJITつきよりも速い
  • TruffleRubyは単に他のRuby実装に比べて別格で、幾何平均で全体で6.23倍の速度向上がある

CRuby 2.0が実行できる7つのベンチマークでは、幾何平均でCRuby 3.1 MJITはCRuby 2.0の1.74倍高速(binarytrees 1.9倍、erubi 1.25倍、fannkuchredux 1.33倍、nbody 2.17倍、optcarrot 3.0倍、psych-load 1.12倍、rubykon 2.08倍)です。これは3倍というより2倍に近い(つまりRuby 3×3)ですが、CRuby 2.0から大きく改善されています。

Conclusion

数字が物語っていると思います。

TruffleRubyのリーダーとして、この結果について私が思うに、TruffleRubyは他のどのRubyの実装よりも、Rubyのコードを理解し最適化できる、ということです。他のRuby実装がTruffleRubyの性能レベルに到達することはないだろうと思われます。TruffleRuby は、現在利用可能な、最も高度なJITコンパイラの一つであるGraalVM JITコンパイラを使用しています。

GraalVM
https://www.graalvm.org/java/

TruffleRubyは、Truffleフレームワークを通じて、JITと直接通信しているため(TruffleRubyチームとGraalVMコンパイラチームも同様です)、最適化/プロファイリング/インライン化の価値があるものとないものを簡単に伝え、いつ最適化を解除してインタープリタに別の方法で再プロファイルまたは再コンパイルすべきかをJITに伝達できるのです。Truffleフレームワークを使用すると、Rubyの多くの部分をより簡単に最適化できます。例えば、インラインキャッシュの追加はわずか2行のコードで済みますが(そのため、TruffleRubyには100以上のインラインキャッシュがあります)、他のVMでは、インラインキャッシュを一つ追加するのに非常に複雑なので、ほとんど使用されていません。最後に、GraalVMのアーキテクチャは、Rubyコード、Javaコード、C拡張、Ruby正規表現、その他GraalVMに実装されたあらゆる言語をインラインでJITコンパイルすることを可能にし、この素晴らしい性能をもたらします。

Just-in-Time Compiling Ruby Regexps on TruffleRuby
https://rubykaigi.org/2021-takeout/presentations/eregontp.html
Architecture Overview of GraalVM
https://www.graalvm.org/docs/introduction/

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト /  変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト /  変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト /  変更 )

%s と連携中