#RubyKaigi 2018 3日目に参加しました。

f:id:Watson:20180602120040j:plain

途中からぬけて、嫁と合流して仙台市内を観光したりしてました。

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

  • 3年以上 TruffleRuby のことやっているそう
  • CRuby と C extension のフルサポートをターゲッティング
    • CRuby 2.5.1 より起動が速い (25ms vs 44 ms)
    • 実行速度は3倍以上
  • OptCarrot
    • CRuby 2.0 → 28 fps
    • TruffleRuby with JIT → 200+ fps
  • C extesion を動かすにはパッチが必要になったりする
    • 改善はされそう
  • TruffleRuby の JIT はすごいね
    • 途中で定数になると、つぎつぎインライン展開していて、最終的に定数を返すだけになっていた
    • CRuby の JIT だと runtime の C API を呼び出すところでどうしてもインライン展開できなくなる
      • (runtime も Ruby で書けば最適化できるんだろうが Object の allocation あたりが難しそう)

並列とスレッドセーフ

  • CRuby はスレッドが同時実行しない
  • JRuby や Rubinius はスレッドセーフでないので Array#<< を並列にやるとだめ
    • Mutex#synchronize 使えば大丈夫だが、不要な際にはオーバヘッドになる
    • Object のインスタンス変数とかも同様
  • スレッドセーフの仕組みはまだ master には入ってないが、Guild みたいな仕組み無しでちゃんと動いてそう

Grow and Shrink - Dynamically Extending the Ruby VM Stack

  • 研究室では以下のcontributeしてきたそう。
    • String#encode
    • String#normalize
    • String#upcase
    • Unicode 11

スタックのサイズはデフォルト 1MB でマルチスレッドでいろいろやっていると足りなくなることがある。

  • 10000スレッドあれば 1MB x 10000 = 10GB

Ruby VM Stack

2 つのスタックを使用している

1. call stack

  • Controll frame で使用している
  • メソッドを使用すると frame が増える
  • Object#show_stack

2. Internal stack

  • controll frame で使用しているがサイズは不定
  • 命令実行時の際に使用される

スタック拡張の実装

  • スタックのサイズはデフォルト 1MB
  • スタックオーバーフローしたら2倍に拡張する
    • オーバーフローを検知し、例外を検出したら2倍にした領域を allocation し、データをコピーしたら古い方を解放
    • ローカル変数の参照を修正する
    • スタック内のポインターを修正する
    • メソッド呼び出し時のメソッド引数のポインター
  • 開発時の話(Debug しやすくするため)
    • 古い方のスタック領域をアクセス禁止にし、アクセスがあればすぐに SEGV するようにした。
    • スタック切り替えを頻繁にした

評価

  • 最大で 53 % の低下があった。平均19%
  • call stack の参照で遅くなっていた
    • call stack を linked list にした
    • 平均7%の低下になった。
  • スタックが浅いスレッドがたくさんあるような現実的な条件だとメモリ使用量は trunk より低下した
  • 他の言語でもなんらかの Stack 拡張が行われているが、CRuby には無かった

所感

fork-exec 時の評価あると良かったかなぁと。

The Method JIT Compiler for Ruby 2.6

早速資料が公開されてました。ありがとうございます!

Current Status

  • Ruby 2.6.0-preview2 はまだ壊れている
  • race condition がある
  • Bytecode を C のコードを生成し、gccやclangでコンパイル
    • JIT の実装は自動的に生成される設計

パフォーマンス

  • 2017年のLT発表の時は4.96倍 → 今回は 5.7 倍
  • OptCarrot で2倍くらい

JIT on Rails

  • Rails で使うと遅くなる。以下の原因を予想した。
    • longjump が遅くなる?
    • Profiling method のオーバーヘッド
    • JIT で呼び出したメソッドがキャンセルされてる?
    • JIT のコンパイルがオーバーヘッド
    • JIT したコードの呼び出しのオーバーヘッド?

longjump が遅くなる?

予想と違った

Profiling method のオーバーヘッド?

予想と違った

JITで呼び出したメソッドがキャンセルされてる?

  • JITで動かせないようなメソッドはキャンセルして VM での実行にフォールバックしている
    • メソッドが再定義されたりしたケース
  • Rails だとそこそこある

JIT のコンパイルがオーバーヘッド

メインスレッドから JIT スレッドに処理を転送するところのオーバーヘッドなど。 Rails が動き始めれば影響はないはず

JIT したコードの呼び出しのオーバーヘッド?

遅くなるケースがあった → 少し改善した

他の原因は?

たくさんの異なるメソッドを呼びまくると遅くなる

  • icache がヒットしなくて、メソッド探索に時間がかかっている
    • コンパイル済みの so ファイルが大量にあるせいらしい
    • ひとつにまとめるとパフォーマンスがあがった

Method Inlining

C コンパイラにたくさん最適化してもらうために必要な処理

  1. JITコンパイラが呼び出すメソッドの定義を知っている
  2. JITコンパイラが呼び出すコードを変更できること
  3. (聞き逃した)

  4. Ruby メソッド

    • Ruby メソッドから呼ばれるケースは楽
    • C メソッド hard
  5. Ruby block
    • Ruby メソッドから yield するのはまあまあ
    • C メソッド hard
  6. C メソッド
    • Ruby メソッドから呼ばれるケースはまあまあ
    • C メソッド hard

C で書かれた部分を Ruby で書き直すとか。

  • Integer#times で仮にためしたら 2.56 倍速くなった

C language is dead