Skip to content

Instantly share code, notes, and snippets.

@wanabe
Last active September 2, 2018 10:53
Show Gist options
  • Select an option

  • Save wanabe/a9dfd097598067bbb67fb760d7fab745 to your computer and use it in GitHub Desktop.

Select an option

Save wanabe/a9dfd097598067bbb67fb760d7fab745 to your computer and use it in GitHub Desktop.
2018/09/02 時点の trunk の CRuby で MJIT 有効にしたときの Rails のパフォーマンスのネックを探る作業記録

この文章は

https://gist.github.com/wanabe/66642ae87c52f9bebf3d06a4f8c55b43 の続きで、題名の通りの作業記録です。 だいぶしつこいですがまたやります。 例によって例のごとくまったく未完成で書きながら作業しているので、いつ終わるのか何か成果が出るのかわかりません。

とっかかり

CRuby の MJIT は日々進歩しています。 特に、最近 r64094 で追加された "JIT compaction" は MJIT 有効時の Rails の速度改善にかなり効果が高いようです。 が、とはいえ手元だとやっぱりまだ遅い気がするので、どうなっているのか見ていきたいと思います。

現状

例によって rails new でできた Rails アプリケーションにベンチのための変更を加えたもの https://github.com/wanabe/rails-new-benchmark で見ていきます。

こんな感じ のスクリプトを適当に作って、得られたプロットが こちら です。 だいたい 2 割強ほど遅くなっている感じでしょうか。

単純化

実行するスクリプトを小さくしてみるとどうなるか確認することにします。 ベンチマークスクリプトを 上書きするスクリプト と、その補助スクリプト を用意しました。 この状態で rm bench.log; ./bench.sh 3_000_000 などとした実行結果が こちら です。 このとき出力を見ていると 1 つも JIT されたメソッド・ブロックはなかったようなので、jit と nojit の違いは単純に誤差のみと考えられます。

単純化スクリプト概要

本筋とはあまり関係ありませんが、単純化スクリプトその補助スクリプト を簡単に説明します。

単純化スクリプトは、以下のものを指定して使います。

  • 上書き対象のクラス $caller_class
  • 上書き対象のメソッド $caller_method_name
  • 環境変数 SHOW がセットされているとき
    • 注目するメソッドのレシーバ $callee_receiver_name
    • 注目するメソッド $callee_method_name
  • 環境変数 SHOW がセットされていないとき
    • 単純化するときのダミーの返り値 $stub_result

たとえば、Object#call_app から呼び出される @app.get メソッドの定義を確認したいときには $caller_class = Object / $caller_method_name = :call_app / $callee_receiver_name = "@app" / $callee_method_name = :get とした状態で SECRET_KEY_BASE="dummy" RAILS_ENV=production bundle exec rake bench_raw SHOW=1 とコマンドを実行すると、以下のような出力が得られます。

class Rack::MockRequest
    def get(uri, opts={})     request(GET, uri, opts)     end
end

ここから $caller_class = Rack::MockRequest / $caller_method_name = :request / $stub_result = OpenStruct.new(status: 200) として bin/rake bench_raw とコマンドを実行すると、Rack::MockRequest#request の実装を単純化した状態でベンチマークが取れる、ということです。 (OpenStruct.new(status: 200) の部分は、後続のスクリプトがエラーにならないように適当に選んでいます)

補助スクリプトは、メソッド定義の最初の行と最後の行を取得するために使っています。

こんな感じで実装の確認と単純化を交互に行っていきます。

#!/bin/sh
set -e
export RBENV_VERSION=trunk
nojit=""
jit="--jit --jit-verbose=1 --jit-min-calls=20000"
if [ -z "$1" ]; then
N=2000
else
N=$1
fi
if [ -f bench.log ]; then
echo "Skip bench" 1>&2
else
echo "Run bench" 1>&2
for type in nojit jit; do
RAILS_ENV=production `rbenv which ruby` $(eval "echo \"\$$type\"") -W0 `rbenv which bundle` exec rake bench_raw \
RAILS_ENV=production N=$N L=10 TRAINING_NUM=18000 WAIT_SEC=1 \
| sed -u -e "s/[0-9.]* i.s/$type &/"
done \
| tee bench.log
fi
gnuplot -e 'set xtics rotate by -15; set terminal png; set out "bench.png"; plot "bench.log" using (1.0):2:(0):1 title "i / s" with boxplot'
require "fiddle/import"
class RubyVM::InstructionSequence
module InternalFunction
extend Fiddle::Importer
dlload Fiddle::Handle::DEFAULT
if Fiddle::SIZEOF_LONG == Fiddle::SIZEOF_VOIDP
typealias "VALUE", "unsigned long"
elsif Fiddle::SIZEOF_LONG_LONG == Fiddle::SIZEOF_VOIDP
typealias "VALUE", "unsigned long long"
end
extern "const rb_iseq_t *rb_iseqw_to_iseq(VALUE iseqw)"
begin
extern "void rb_iseq_code_location(const rb_iseq_t *iseq, int *first_lineno, int *first_column, int *last_lineno, int *last_column)"
rescue Fiddle::DLError
extern "void rb_iseq_code_range(const rb_iseq_t *iseq, int *first_lineno, int *first_column, int *last_lineno, int *last_column)"
singleton_class.alias_method :rb_iseq_code_location, :rb_iseq_code_range
end
end
def first_column
code_locations[1]
end
def last_lineno
code_locations[2]
end
def last_column
code_locations[3]
end
def code_locations
size = Fiddle::SIZEOF_INT
buf = "\0" * (size * 4)
ptr = Fiddle::Pointer[buf]
iseq = InternalFunction.rb_iseqw_to_iseq(Fiddle.dlwrap(self))
InternalFunction.rb_iseq_code_location(iseq, ptr, ptr + size, ptr + size * 2, ptr + size * 3)
buf.unpack("i4")
end
end
ISeq = RubyVM::InstructionSequence
module IseqContainer
def to_iseq
ISeq.of(self)
end
end
class Method
include IseqContainer
end
require_relative "iseq"
$caller_class = Object
$caller_method_name = :call_app
$callee_receiver_name = "@app"
$callee_method_name = :request
$caller_class = Rack::MockRequest
$caller_method_name = :request
$callee_receiver_name = "@app"
$callee_method_name = :call
$stub_result = OpenStruct.new(status: 200)
$caller_class.module_exec do
if ENV["SHOW"]
define_method($caller_method_name) do |*a|
receiver = eval $callee_receiver_name
meth = receiver.method($callee_method_name)
first, _, last, _ = meth.to_iseq.code_locations
path, _ = meth.source_location
lines = File.readlines(path)[(first - 1)..(last - 1)]
klass = receiver.class.ancestors.find(&:name)
puts "class #{klass}", *lines, "end"
exit
end
else
define_method($caller_method_name) do |*a|
return $stub_result
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment