原文連接: 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功能所進行的嘗試,像rujit,已經讓Ruby可以成功提速,不過會耗費過多的內存。另外一個嘗試,OMR + Ruby使用了已有的JIT程序庫Eclipse OMR。還有其餘案例llrb,它使用基於LLVM的JIT庫。這些實現可以被預見的最大問題是JIT庫都是活靶子,會把Ruby的倖存者帶到一個未知的將來。緩存
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
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已經發布,能夠直接嘗試穩定版)。
爲了運行你的代碼,Ruby必需要經歷一些步驟。首先,代碼被令牌化,解析,而且編譯成YARV指令。這部分流程大概會佔用Ruby程序運行時間的30%。
咱們能夠經過使用標準庫中的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..."
複製代碼
yomikomu和bootsnap將會向你展現經過把YARV指令緩存到磁盤上來提升Ruby的運行速度。這樣作的話,當Ruby腳本第一次運行完以後,指令不須要再次被解析以並編譯成YARV,除非你修改了代碼。固然,這不會爲Ruby的首次運行提高速度,而會爲後續的執行提速百分之30-由於跳過了解析而且編譯成YARV指令這個步驟。
這個緩存編譯好的YARV指令的策略實際上並無JIT相關的工做,不過這個策略已經在Rails5.2裏面使用了(經過bootsnap)極可能也會在將來的Ruby版本中出現。目前的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從新計算。
你能夠經過添加--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倍的時間。
不管你使用的是GCC仍是Clang都會對啓動時間有所影響。現在,GCC被認爲是比Clang更快的編譯器,可是依舊會在帶着JIT啓動的時候多耗費3倍左右的時間。
在這種狀況下,你可能不會想要在任何存活時間很是短的程序中開啓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.
這是一個很大的性能提高!爲Ruby添加JIT以後已經讓Ruby2.6朝着3X3的提案邁出一大步。並且剛開始的JIT對性能的提高是很是保守的,MJIT的引入並無採用許多在RTL MJIT身上可以看到的優化方案。即使沒有采用這些優化方案,性能的提高仍是十分顯著的。在一些額外的優化被引入以後,性能可能會更加可觀吧。
下面的beanchmark展現了optcarrot在分別在多個版本的Ruby上運行的狀況(前180視頻幀),在Ruby2.5和Ruby2.0上展示出很是平滑的性能表現。TruffleRuby, JRuby還有Topaz都是目前已經有JIT功能的Ruby實現。你能夠看到這些帶有JIT功能的實現(下面綠色,紅色還有紫色的線條)啓動比較緩慢而且爲了熱身會花費掉一些視頻幀。
在熱身以後TruffleRuby在它那被高度優化的GraalVMJIT的支持下性能遙遙領先。
官方的optcarrot benchmark目前還沒包含Ruby2.6-dev開啓JIT以後的測試結果,不過它還不能與TruffleRuby相抗衡。TruffleRuby雖然說性能比起其餘實現要領先很多,不過尚未爲生產級別作好準備。
修改optcarrot benchmark以展現Ruby2.6-dev在基於GCC開啓JIT功能的運行狀況,咱們能夠看到爲了熱身它會消耗掉一些幀。在熱身以後,即使有許多優化沒有被開啓,它也可以與早些版本的實現拉開距離。注意綠色的線條啓動緩慢,然而一旦追上來,便會持續保持領先。
若是咱們放大來看,咱們能夠看到基於GCC並開啓了JIT的Ruby2.6-dev在80幀這個點左右將會與Ruby2.5拉開距離-在benchmark中只是佔用幾秒的時間而已。
若是你的Ruby程序存活時間比較短,幾秒以後就退出了,你可能不會想要開啓新的JIT功能。然而若是你的程序將會運行會運行更長的時間,而且你有必定的空閒的內存,那麼JIT可能會帶來可觀的性能優點。
在Square裏咱們內部大量使用Ruby,咱們維護了許多Ruby開源項目包括了鏈接Square用的Ruby的SDK。所以對咱們來講在CRuby中JIT的新特性是激動人心的。而在聖誕節發佈以前,還有很多的工做要作,還要引入一些較爲容易實現的優化方案。如今請在Ruby的trunk或者nightly版本中嘗試JIT功能,並報告你遇到的問題。
Vladimir Makarov和Takashi Kokubun爲Ruby引入了JIT並把Ruby往前推動這件事情上應受很大的讚譽,相信在接下來的幾年還會帶來更多性能方面的改善。
想了解更多?註冊咱們每月的開發者新聞。