【译】Ruby的新特性JIT

原文连接: medium.com/square-corn… Ruby2.6已经发布了一个多月了,这篇文章显得有点老旧,不过仍是有助于理解JIT究竟是个什么东西,它是如何提高Ruby的运行速度的,以及社区为了在Ruby里添加JIT所做的努力。html


CRuby有JIT了。git

为了给Ruby实现JIT功能已经进行过许多尝试,这些参考实现一直以来都没可以被合并,直到今天咱们终于有JIT了。github

,---.    ,---.     .-./`)  .-./`) ,---------.
  .     ' , | \ / | \ '_ .')\ .-.')\          \
    _________      |  ,  \/  ,  |    (_ (_) _)/ `-' \ `--. ,---'
   /_\/_ _ \/_\    |  |\_   /|  |      / .  \  `-'`"` | \ \ \ / / | _( )_/ | | ___ | '`|   .---.     :   :
  ,   \\  //   .   | (_ J _) |  ||   | |   ' | | | | \/ | (_,_) | || `-'  /    |   |     |   |
     ,      .      |  |      |  | \      /     |   |     |   |
                   '--'      '--'  `-..-' '---' '---' 复制代码

Ruby2.6将有一个可选的--jit标记用来启用JIT功能,这会增长应用启动的时间而且会耗费更多的内存,都是为了在应用启动就绪以后可以得到耀眼的运行速度。编程

早期的Ruby JIT尝试

这里有一些早期为Ruby添加JIT功能所进行的尝试,像rujit,已经让Ruby可以成功提速,不过会耗费过多的内存。另外一个尝试,OMR + Ruby使用了已有的JIT程序库Eclipse OMR。还有其余案例llrb,它使用基于LLVM的JIT库。这些实现可以被预见的最大问题是JIT库都是活靶子,会把Ruby的幸存者带到一个未知的将来。缓存

一个大的飞跃:RTL MJIT

Vladimir Makarov为Ruby的性能提高做出了很多贡献,他在Ruby2.4里从新实现了Hash表很大程度地为hash访问提速。安全

在2017年,Makarov主要在跟进一个新的项目,被称为RTL MJIT,重写了Ruby中间表现的工做方式并为Ruby添加了JIT。在这个很是有野心的项目里,已经存在的YARV指令集彻底被崭新的指令集RTL(寄存器传输语言)所取代。Makarov同时也建立了一个被称为MJIT的JIT编译器,将会根据RTL指令产生C代码,而后经过已有的编译器把C代码编译成原生机器代码。ruby

Makarov的实现的最大问题就是要使用崭新的RTL意味着对Ruby内部的大规模重写。可能还得耗费一些年的时间来打磨相关的工做,直到某个时间点功能能够稳定并为合并到Ruby中作好准备。Ruby3应该会绑定这个新的RTL指令集,不过还不可以肯定(应该是几年后的事情了)。bash

已经被合并到Ruby中的JIT:YARV MJIT

Takashi Kokubun为Ruby的JIT以及性能提高方面作了很多贡献。他是llrbJIT的做者,在Ruby2.5的开发过程当中屡次提高了Ruby的ERB和RDoc的生成速度。oracle

Kokubun基于Makarov在RTL MJIT的工做成果,从中抽取了JIT功能部分,并保留了Ruby已有的YARV字节码。他对MJIT的功能进行缩减,只保留能够知足需求的最小形式,去除掉了一些较为高级的优化,所以它能够被引入到已有的Ruby中,而并不会破坏Ruby其余部分的功能。less

__  __              _            ___            _____
      |  \/  |          _ | |          |_ _|          |_   _|
      | |\/| |         | || |           | |             | |
      |_|__|_| _____   _\__/   _____   |___|   _____   _|_|_
     _|"""""|_|"""""|_|"""""|_|"""""|_|"""""|_|"""""|_|"""""|倭 "`-0-0-'"`-0-0-'"`-0-0-'"`-0-0-'"`-0-0-'"`-0-0-'"`-0-0-' 复制代码

Kokubun的工做已经被合并到Ruby中,会随着Ruby2.6在2018年圣诞节那天发布出去。若是你想要如今就尝试JIT,你可使用Ruby的构建版。在这个moment性能的提高仍是至关保守的,在Ruby2.6发布以前还会在优化上耗费大量的时间。Kokubun的策略是先保证安全,而后再逐步优化已有的工做。因而Ruby有JIT了。(翻译这篇文章的时候Ruby2.6已经发布,能够直接尝试稳定版)。

它是怎么工做的

获取YARV指令集

JIT

为了运行你的代码,Ruby必需要经历一些步骤。首先,代码被令牌化,解析,而且编译成YARV指令。这部分流程大概会占用Ruby程序运行时间的30%。

Move

咱们能够经过使用标准库中的RubyVM::InstructionSequence以及Ripper来观察上面提到的每个步骤。

require 'ripper'

##
# Ruby Code
code = '3 + 3'

##
# Tokens
Ripper.tokenize code
#=> ["3", " ", "+", " ", "3"]

##
# S-Expression
Ripper.sexp code
#=> [:program, [[:binary, [:@int, "3", [1, 0]], :+, [:@int, "3", [1, 4]]]]]

##
# YARV Instructions
puts RubyVM::InstructionSequence.compile(code).disasm
#>> == disasm: #<ISeq:<compiled>@<compiled>:1 (1,0)-(1,5)>==================
#>> 0000 putobject 3 ( 1)[Li]
#>> 0002 putobject 3
#>> 0004 opt_plus <callinfo!mid:+, argc:1, ARGS_SIMPLE>, <callcache>
#>> 0007 leave

##
# YARV Bytecode
RubyVM::InstructionSequence.compile(code).to_binary
#=> "YARB\x02\x00\x00\x00\x06\x00\x00\x003\x02\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\xA4\x01\x00\x00\xA8\x01\x00\x00..."
复制代码

yomikomubootsnap将会向你展现经过把YARV指令缓存到磁盘上来提升Ruby的运行速度。这样作的话,当Ruby脚本第一次运行完以后,指令不须要再次被解析以并编译成YARV,除非你修改了代码。固然,这不会为Ruby的首次运行提高速度,而会为后续的执行提速百分之30-由于跳过了解析而且编译成YARV指令这个步骤。

这个缓存编译好的YARV指令的策略实际上并无JIT相关的工做,不过这个策略已经在Rails5.2里面使用了(经过bootsnap)极可能也会在将来的Ruby版本中出现。目前的JIT只有在YARV指令存在的状况下才会工做。

JIT编译YARV指令集

当YARV指令存在的时候,RubyVM在运行时的职责就是把这些指令集转换成能适应你正在使用的操做系统以及CPU的原生机器代码。这个过程会占用运行Ruby程序70%的时间,大块的运行时间。

这也是JIT发挥做用的地方。并非每次遇到YARV指令集都会对它进行计算,其中的某些调用可以被转换成原生的机器代码,之后再次赶上的时候便能直接使用原生代码了。

这是一个ERB模版,会生成Ruby代码,生成C代码,经过JIT来生成C代码。~mjit_compile.inc.erb

使用MJIT的时候,某些Ruby的YARV指令集会转换成C代码而且会放置在.c文件当中,它们会被GCC或者Clang编译成名字为*.so的动态库文件。RubyVM能够在下次看到相同的YARV指令时从动态库中直接使用缓存好的而且通过预编译的原生机器码。

逆优化

然而,Ruby是一门动态类型的编程语言,即使是核心的类方法都可以在运行时从新定义。这须要一些机制去检测已经被缓存到原生代码中的调用有没有被从新定义。若是这些调用被从新定义,则须要刷新缓存。这些指令就像是在没有JIT的环境那样被正常解释。当一些东西有所改变的时回退到计算指令集的过程被称为逆优化

##
# YARV instructions for `3 + 3`:
RubyVM::InstructionSequence.compile('3 + 3').to_a.last
#=> [1,
 :RUBY_EVENT_LINE,
 [:putobject, 3],
 [:putobject, 3],
 [:opt_plus, {:mid=>:+, :flag=>16, :orig_argc=>1}, false],
 [:leave]]
##
# MJIT C code created from the `:opt_plus` instruction above:
VALUE opt_plus(a, b) {
  if (not_redefined(int_plus)) {
    return a + b;
  } else {
    return vm_exec();
  }
}
复制代码

记住,在上面的例子中,若是调用被从新定义,MJIT所产生的C代码会优化逆行并从新计算指令集。大部分时间里咱们都不会从新定义加法运算,这为咱们带来好处,所以咱们能够利用JIT去使用已经编译好的原生代码。每一次C代码被执行,它会确认它优化过的操做有没有改变。若是有所改变,就是逆优化,指令集会被RubyVM从新计算。

Deoptimization

使用JIT

你能够经过添加--jit标志来使用JIT。

$ ruby --jit -e "puts RubyVM::MJIT.enabled?"
true
复制代码

还有许多试验性的与JIT相关的标志位选项:

MJIT options (experimental):
  --jit-warnings  Enable printing MJIT warnings
  --jit-debug     Enable MJIT debugging (very slow)
  --jit-wait      Wait until JIT compilation is finished everytime (for testing)
  --jit-save-temps
                  Save MJIT temporary files in $TMP or /tmp (for testing)
  --jit-verbose=num
                  Print MJIT logs of level num or less to stderr (default: 0)
  --jit-max-cache=num
                  Max number of methods to be JIT-ed in a cache (default: 1000)
  --jit-min-calls=num
                  Number of calls to trigger JIT (for testing, default: 5)
复制代码

你能够在IRB里交互式地使用JIT

$ ruby --jit -S irb
irb(main):001:0> RubyVM::MJIT.enabled?
=> true
复制代码

这是早期的代码调试工具,JIT固然也可以在Pry中工做

$ ruby --jit -S pry
pry(main)> RubyVM::MJIT.enabled?
=> true
复制代码

启动时间

启动时间是使用新的JIT功能的时候须要考虑的一件事情。启动Ruby时伴随着JIT的功能会多耗费大概6倍的时间。

time

不管你使用的是GCC仍是Clang都会对启动时间有所影响。现在,GCC被认为是比Clang更快的编译器,可是依旧会在带着JIT启动的时候多耗费3倍左右的时间。

time with gcc

在这种状况下,你可能不会想要在任何存活时间很是短的程序中开启JIT功能。不只是JIT须要启动,为了高效,它可能还须要一些时间来热身(一些预编译)。在运行时间较长的程序中使用JIT性能表现会十分突出-它能够充分热身并有机会使用已经缓存好的原生机器码。

性能

2015年,Matz提到了3x3宣称Ruby3.0将要比2.0快3倍。官方的Ruby3x3的测量工具是optcarrot,一个用Ruby写的任天堂仿真器。

现实中任天堂运行的帧率是60FPS。Kokubun’s 在一台8核心4GHZ的机器上用optcarrot作过一个benchmarks显示出Ruby2.0帧率是35FPS,Ruby2.5帧率是46FPS提高了大概百分之30。在JIT开启的状况下Ruby2.6比Ruby2.0快了将近百分之80,帧率达到63FPS.

optcarrot

这是一个很大的性能提高!为Ruby添加JIT以后已经让Ruby2.6朝着3X3的提案迈出一大步。并且刚开始的JIT对性能的提高是很是保守的,MJIT的引入并无采用许多在RTL MJIT身上可以看到的优化方案。即使没有采用这些优化方案,性能的提高仍是十分显著的。在一些额外的优化被引入以后,性能可能会更加可观吧。

下面的beanchmark展现了optcarrot在分别在多个版本的Ruby上运行的状况(前180视频帧),在Ruby2.5和Ruby2.0上展示出很是平滑的性能表现。TruffleRuby, JRuby还有Topaz都是目前已经有JIT功能的Ruby实现。你能够看到这些带有JIT功能的实现(下面绿色,红色还有紫色的线条)启动比较缓慢而且为了热身会花费掉一些视频帧。

Image by Yusuke Endoh, distributed under MIT license.

在热身以后TruffleRuby在它那被高度优化的GraalVMJIT的支持下性能遥遥领先。

mage by Yusuke Endoh, distributed under MIT license.

官方的optcarrot benchmark目前还没包含Ruby2.6-dev开启JIT以后的测试结果,不过它还不能与TruffleRuby相抗衡。TruffleRuby虽然说性能比起其余实现要领先很多,不过尚未为生产级别作好准备。

修改optcarrot benchmark以展现Ruby2.6-dev在基于GCC开启JIT功能的运行状况,咱们能够看到为了热身它会消耗掉一些帧。在热身以后,即使有许多优化没有被开启,它也可以与早些版本的实现拉开距离。注意绿色的线条启动缓慢,然而一旦追上来,便会持续保持领先。

Ruby2.6-dev

若是咱们放大来看,咱们能够看到基于GCC并开启了JIT的Ruby2.6-dev在80帧这个点左右将会与Ruby2.5拉开距离-在benchmark中只是占用几秒的时间而已。

frames

若是你的Ruby程序存活时间比较短,几秒以后就退出了,你可能不会想要开启新的JIT功能。然而若是你的程序将会运行会运行更长的时间,而且你有必定的空闲的内存,那么JIT可能会带来可观的性能优点。

将来

在Square里咱们内部大量使用Ruby,咱们维护了许多Ruby开源项目包括了链接Square用的Ruby的SDK。所以对咱们来讲在CRuby中JIT的新特性是激动人心的。而在圣诞节发布以前,还有很多的工做要作,还要引入一些较为容易实现的优化方案。如今请在Ruby的trunk或者nightly版本中尝试JIT功能,并报告你遇到的问题。

Vladimir MakarovTakashi Kokubun为Ruby引入了JIT并把Ruby往前推动这件事情上应受很大的赞誉,相信在接下来的几年还会带来更多性能方面的改善。

想了解更多?注册咱们每月的开发者新闻。

相关文章
相关标签/搜索