The method JIT compiler for Ruby 2.6

What's "MJIT"?

Let me share the summary about recent JIT topics on Ruby. Those who read the above articles may want to skip this section.

The RTL instruction project

In July 2016, Vladimir Makarov, or Vlad, who optimized Hash at Ruby 2.4 filed a ticket "VM performance improvement proposal" (Feature #12589). It's a project to replace all Ruby VM's stack-based instruction set with register-based one, which is named RTL (Register Transfer Language) instructions.

MJIT: JIT compiler that runs on RTL instructions

To approach the problem, in March 2017, he also published JIT compiler which compiles RTL instructions, which is named MJIT (MRI JIT).

YARV-MJIT: Yet another JIT compiler that runs on YARV instructions

In this way, RTL instruction conversion was a somewhat risky change, but my company implements cloud service as Rails application and provides it, so very high availability is required. If high risk changes were made to Ruby, upgrading Ruby would be very difficult.

Ruby's JIT status

Ruby 2.6 merged following 2 components.

  • JIT Compiler: YARV-MJIT
  • JIT Infrastructure: MJIT

JIT compiler benchmarks

Even though the conservative JIT compiler has the aim to minimize risks of JIT introduction, of course it must improve the performance because the change is for optimization. I'll share some benchmark results on my machine: Intel 4.0GHz i7-4790K 8 Cores, 16GB memory, x86_64 Linux.

Optcarrot

Benchmark in “Playing with ruby’s new JIT”

Is the JIT compiler's optimization limited to only about 11%?

def calculate(a, b, n = 40_000_000)
i = 0
c = 0
while i < n
a = a * 16807 % 2147483647
b = b * 48271 % 2147483647
c += 1 if (a & 0xffff) == (b & 0xffff)
i += 1
end
c
end
Benchmark.ips do |x|
x.iterations = 3
x.report("calculate") do |times|
calculate(65, 8921, 100_000)
end
end
$ ruby -v
ruby 2.6.0dev (2018-02-15 trunk 62410) [x86_64-linux]
$ ruby bench.rb
Warming up --------------------------------------
calculate 13.000 i/100ms
calculate 13.000 i/100ms
calculate 13.000 i/100ms
Calculating -------------------------------------
calculate 1.800k (± 2.7%) i/s - 8.996k in 5.002504s
calculate 1.785k (± 7.4%) i/s - 8.853k in 5.003616s
calculate 1.802k (± 4.0%) i/s - 8.996k in 5.006199s
$ ruby --jit bench.rb
Warming up --------------------------------------
calculate 13.000 i/100ms
calculate 18.000 i/100ms
calculate 27.000 i/100ms
Calculating -------------------------------------
calculate 7.182k (± 9.1%) i/s - 35.397k in 5.000332s
calculate 7.296k (± 2.9%) i/s - 36.450k in 5.001392s
calculate 7.295k (± 3.1%) i/s - 36.450k in 5.002572s

Other benchmarks

The commit merging JIT compiler shows other benchmark results in the commit message. Some of micro benchmarks are improved about 2x faster compared to Ruby 2.6's VM execution.

VM-Generated JIT Compiler

As JIT compiler needs to generate code that works in the same way as VM implementation, a very simple JIT compiler would have many copy-pastes of VM implementation. It would be bad for maintainability.

switch (insn) {
% RubyVM::BareInstructions.to_a.each do |insn|
case BIN(<%= insn.name %>):
<%= render 'mjit_compile_insn', locals: { insn: insn } -%>
break;
% end
}
% expand_simple_macros.call(insn.expr.expr).each_line do |line|
% if line =~ /\A\s+JUMP\((?<dest>[^)]+)\);\s+\z/
/* Dynamic generation of JUMP code */
% else
fprintf(f, <%= to_cstr.call(line) %>);
% end
% end
switch (insn) {
case BIN(nop):
fprintf(f, "{\n");
{
fprintf(f, " reg_cfp->pc = original_body_iseq + %d;\n", pos);
fprintf(f, " reg_cfp->sp = (VALUE *)reg_cfp->bp + %d;\n", b->stack_size + 1);
fprintf(f, " {\n");
fprintf(f, " /* none */\n");
fprintf(f, " }\n");
b->stack_size += attr_sp_inc_nop();
}
fprintf(f, "}\n");
break;
case BIN(getlocal):
/* ... */
}
VALUE
_mjit0(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp)
{
VALUE *stack = reg_cfp->sp;
static const VALUE *const original_body_iseq = (VALUE *)0x5643d9a852a0;
if (reg_cfp->pc != original_body_iseq) {
return Qundef;
}
label_0: /* nop */
{
reg_cfp->pc = original_body_iseq + 1;
reg_cfp->sp = (VALUE *)reg_cfp->bp + 2;
{
/* none */
}
}
/* snip... */} /* end of _mjit0 */

How is JIT infrastructure?

Support status of platforms

I could easily port pthread to Windows native thread using existing thread abstraction layer for Ruby.

  • NetBSD
  • Solaris
  • AIX
  • Intel C/C++ Compiler
  • Old Visual Studio

Security

Other core committers (nobu, usa) has improved many parts of JIT infrastructure. One of them is security.

Others

About "Startup Time"

The article Ruby's New JIT pointed out that:

  • Building precompiled header on Ruby's build, not on runtime
  • Immediately cancel JIT compiler thread regardless of its state

`--jit-cc` does no longer exist

Sometimes C compiler fails to compile a header generated by another C compiler. It's not Ruby's fault and we're not going to support it.

Acknowledgements

I'm still not sure if my JIT compiler will be in the release of Ruby 2.6, but I want to say "Thank you" for following people.

  • The inventor of MJIT: Vladimir Makarov
  • Ruby's father: Matz
  • Reviewers of YARV-MJIT code: ko1, mame
  • Many bug reports and fixes: wanabe
  • The first MinGW support patch: Lars Kannis
  • Maintainer of many build environments: hsbt
  • Core committers fixing MJIT infrastructure: nobu, usa, znz, knu
  • Recent VM improvements: shyouhei

What's next?

We're preparing to release very early preview of Ruby 2.6.0. The performance may not be improved so much at the release, but I hope you enjoy hacking MJIT there.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store