Ruby JIT for MRI 开始开发了

ceclinux-github · 2018年02月08日 · 最后由 fenginsc 回复于 2020年06月15日 · 10316 次阅读
本帖已被管理员设置为精华贴

来自这个 commit: https://github.com/ruby/ruby/commit/ed935aa5be0e5e6b8d53c3e7d76a9ce395dfa18b, 已经有人迫不及待的开玩了: https://www.johnhawthorn.com/2018/02/playing-with-ruby-jit-mjit/

感想:

  1. JIT 开发依然在非常初级的阶段,很容易写出奇怪的代码来跳过(或触发 JIT 优化)
  2. 性能提升巨大,我的机子上跑 2.6.0-dev --jit 比 2.5.0 快了差不多一倍,非常期待后续提升

怎么我看的对比数据,说的是目前提升还不是很明显呢

huacnlee #3 回复

我这按它的脚本,是这样的

 ceclinux@ceclinux-pc  ~/ruby  973   trunk ?  time ./ruby --disable-gems --jit --jit-wait --jit-verbose=1 15a.rb                                                   ✔  10248  18:46:44 
JIT success (55.3ms): block in <main>@15a.rb:13 -> /tmp/_ruby_mjit_p2514u0.c
JIT success (183.4ms): calculate@15a.rb:1 -> /tmp/_ruby_mjit_p2514u1.c
{:result=>600}
Successful MJIT finish
./ruby --disable-gems --jit --jit-wait --jit-verbose=1 15a.rb  4.84s user 0.03s system 99% cpu 4.880 total


 ceclinux@ceclinux-pc  ~/ruby  972   trunk ?  time ./ruby --disable-gems  15a.rb                                                                                 1 ↵  10250  18:47:17 
{:result=>600}
./ruby --disable-gems 15a.rb  9.08s user 0.00s system 99% cpu 9.087 total

一倍差距算是很大了把?

对 Rails 应用改进不大,他们还在找原因,估计算是 6 的工作了

rails 都卡在 io 了。。。。

known issues: Performance is decreased when Google Chrome is running。这就很搞笑了

pynix #6 回复

Rails 的类过多的方法也阻碍了 JIT 的优化空间,具体需要 @luikore 来讲解下,推上 discourse 的人在跟 kokubun 讨论 jit-friendly 的策略,见 https://twitter.com/k0kubun/status/960112559343878144 这 thread

可以說是腳踩 Graal,吊打 JRuby,3x3 目標立刻實現。三倍是什麼?提升六倍也可以啊。

其實那個 Benchmark, Nv 和 CSeaton 已經回覆了,如果讓 Graal / JVM Warm Up, 不計算一開始的 Startup 時間,TruffleRuby 還是快最快的 MJIT 3 倍左右。

理論上那個是比較舊版的 TruffleRuby, 新版本和剛剛才開源的 SubstrateVM 會更加快。當然,Trade off 是 Memory Usage。

很可惜整個 Ruby 社區對 Java / JVM / Oracle 很有保留。

ksec #10 回复

JRuby 主要是和 MRI 有诸多不兼容的地方。一方面没有引入类似 JNI 的机制做 C 扩展的引用,另一方面像是 Ruby 这几个版本非常重要的 Fiber 特性,一直不跟进。这对 JRuby 的推广有很大的障碍。但是反过来说,Ruby 社区为了保留灵活的演进,在几次劝进下都拒绝搞标准化。事实上最后搞了一个标准化 Ruby ISO,基于那个实现了 mruby,社区反映也是不温不火,两边都有苦难言啊。

dsh0416 #9 回复

仔细看了两遍,回答的很用心,信息量很大,感谢🎉

dsh0416 #9 回复

话说这个实现也糙了一点。。。把 hot block 找出来然后 C 化然后 compile 而且还放在 /tmp。重点是,这样一来就依赖 gcc / clang 了。

我期待的好像不是这样的,有点失望。我期待它能本身提供编译的功能,就是 ruby file.rb 就已经能从 YARV -> 机器码,而不依赖外部工具。不过,实现到那样的 level,估计 Rubinius 会便利一些,后端天然的有 LLVM 垫背。

如果 MJIT 实现了即时 YARV -> Native Code 即时存储到内存空间预备即时调用,我觉得它干脆做成下一个 LLVM 了。

pynix #6 回复

刚好相反,rails 太臃肿了,IO 开销实际上是很小的一部分,服务器真正做的部分只有通过调度、读 request、写 respose。

而你的 HTTP parsing、业务处理(rails 本身有大量的对象映射、抽象、创建和销毁等等)才是最大的问题。这些属于计算密集问题。

mjit 在类型明确一些的 block 是更容易被 jit 编译的,但像 AR(Active Record) 则不然,一层层的抽象几乎不尽相同的数据,动态的元编程动态嵌入方法和属性,阻碍了 old gen code 的形成。

jakit #14 回复

动态的元编程动态嵌入方法和属性,阻碍了 old gen code 的形成。

這個正正是 Truffle 的強項。而且我才剛發現,TruffleRuby 已經支援 Nokogiri 及 OpenSSL。距離能夠真正運行 Rails 日子不遠,十分期待。

jakit #13 回复

我期待它能本身提供编译的功能,就是 ruby file.rb 就已经能从 YARV -> 机器码,而不依赖外部工具。

这不叫 JIT 这叫 AOT。尝试的话 RubyMotion 的 Ruby 就是 AOT 编译的。这些尝试 Ruby 社区都有做过,优缺点也都是很明显的。

rails 太臃肿了,I/O 开销实际上是很小的一部分... 而你的 HTTP parsing、业务处理才是最大的问题。

我们拿比 Rails 轻量很多的 Sinatra 和 mysql2 裸驱动,不用 ActiveRecord 出来跑个 benchmark 好了。

Web Server Basic View View + DB
WEBrick 273 req/s 116 req/s 111 req/s
Thin 1597 req/s 174 req/s 139 req/s
Unicorn 605 req/s 121 req/s 121 req/s

为什么 Thin 在 Basic 的时候碾压其他 Server,因为它 I/O 异步了。为什么 View 和 DB 又变差了呢?因为读取模板和查询数据库是同步的。我写 midori 的时候,把每个请求的栈分配都弄到极轻量,还用了 C extension 做 HTTP Parser,单跑 Hello World 的时候也就比 Thin 再快一倍,而加任何一个 I/O 操作就慢十倍。

谁是「很小的一部分」谁是「很大的一部分」就是这么简单的问题,哪来什么刚好相反。但是只关注 I/O 会限制 Ruby 在 Web 服务器以外别的领域的发展。所以 MJIT 还是非常必要搞的,没人讨厌快,但不希望快是以牺牲其他东西为代价,比如 Rubinius 把代码写到自己也维护不动。而 MJIT 所做的就是这一点。

dsh0416 #16 回复

为什么 Thin 在 Basic 的时候碾压其他 Server,因为它 I/O 异步了。为什么 View 和 DB 又变差了呢?因为读取模板和查询数据库是同步的。

你给出来的数据这很明显,Web 框架帮你找到路径之后调用你的业务逻辑肯定会有 View 渲染和 DB 调用,你看看 View 和 View + DB 的差异并不大,说明 DB IO 效率高了去了。

服务器做的事情,是把 数据 read,然后交给单个 task 线程去处理对应的路径反射你的 Controller 渲染你的 View(太长不写)。你看没有 View 效率多高,最差的情况拖了 6x。让一个性能很好的 Thin 拖得只剩下跟其它一样的水平。

异步不异步是另外的一个问题,服务器本身 schedule socket queue 是可以异步的。

但是你一个 Controller 作的事情比如你写了:

  1. 处理 DB
  2. 计算数据,暂且把数据搞了个斐波那契
  3. 处理 View

难不成你把 Controller 这三件事情分别异步成三个线程,甚至然后做完了再通知去让服务器 response 吗?像移动客户端有些 lib 设置了超时,等你异步做完这些事情,再 write 我客户端估计都已经 close 了。

先不讨论异步,Webrick 的 parse 是 Ruby 写的,比如 WEBrick::HTTPUtils::FormData

拿 Ruby 讨论 IO 就是耍流氓

如果你说用 Go / C++ 在流处理的时候碰到瓶颈,还是值得讨论的。


2018.3.29 更新:

但是只关注 I/O 会限制 Ruby 在 Web 服务器以外别的领域的发展

这一点是认可的,这是在开发通用编程语言,不是开发针对性的工具。

Ruby 就算没有 JIT,跑得比 C++ / Go 慢,也是值得用的,因为至少这样的工具好用而且编码也舒服,良好的 Ruby idiom 和 style 也是很好维护,无需编译,写起来也先行轻松。

jakit #17 回复

异步提升的是并发量,不是减少平均响应时间,不要混为一谈。

jakit #17 回复

你是不是觉得从数据库连接读取数据的时间算做 IO 开销,等待数据库处理的时间不算做 IO 开销?

Rei #18 回复

你误解了我的意思。。。我说的不是 Web 响应时间,我说的是网络通讯

Rei #19 回复

等待数据库处理的时间是数据库本身的问题了,不属于 Ruby 管的范围,你们写 Rails 难不成:

class xxxController
  def xxx
    @data
    Thread.new { // 数据库 -> @data }
  end
end

你确定 View 能渲染出 @data 的内容,你确定渲染的时候 数据库已经读完了?

好,如果你 join 是吧,你可以让这次 puma task processor 的线程等待你执行完毕,但是,这就同步了呀

难不成在一次 html page request(Socket connection)中(也就是非 Ajax),你能告诉我怎么能先 write html content 给浏览器(就是 View)再等数据库异步回调再 write 数据的方式吗?

jakit #21 回复

所以说不要把问题混为一谈,数据库查询的耗时不能用应用层的语言解决,能做的就是不要让 CPU 干等着,转去做别的任务。目前已有的 Thread 已经能一定程度解决问题,但是 Thread 切换支出大、共享内存容易出 bug,并发性能比 go node 等有很大差距,所以在研究引入新的并发模型。

这些问题不是 MJIT 对应的,MJIT 想解决的是运算性能,如果你的应用是计算密集,并且没有多少 IO 开销(例如查数据库),那么可以从 MJIT 获益良多。但是一般 web 应用 IO 占到一半左右,从 MJIT 获益多少要实测才知道。更快的执行速度当然是更好的,例如序列化速度可以加快,这对一些结构复杂的 API 服务很有用。

Rei #22 回复

其实我的回复只是告诉 #7 其实 rails 并没有被 IO 卡,puma 的异步处理还是很优秀的。

希望 mjit 能在 rails 这块庞大的 monolith 发挥作用,随着 SPA 开发模型、前端独立岗位的出现,还是亟需的。

jakit #23 回复

「让一个性能很好的 Thin 拖得只剩下跟其它一样的水平」少开玩笑了,Thin 的优化可以说是非常差了,和其他 Web 服务器比起来最大的区别就是把 I/O 包装成了异步。

puma 只处理 Web 服务器的 I/O,应用层调用 DB 也是 I/O,其背后的调用逻辑和

Thread.new { // 数据库 -> @data }

还真的区别不大,Rails 的实现就是类似于 GCD 一样做了个线程池,很明显这个方法是问题的根源。

你确定 View 能渲染出 @data 的内容,你确定渲染的时候 数据库已经读完了?

当然是可以的,但是这里就不是依靠 Thread 或者什么 join 来解决的问题。这就是并发和并行最大的区别,吞吐量和请求处理时间也不是一个概念。你试试用 em-synchrony 替换掉这个部分,就会得到数倍的性能改善。em-synchrony 也是纯 Ruby 实现的,按理说,往里面加一层东西应该会更慢才对,为什么反而快了好几倍呢?

你对并发和并行的理解太混淆了,以及对 I/O 是什么理解很有问题。View 层和 DB 层都不是在 Ruby 里面处理的,为什么同样调用 mysql2 的驱动,用 goroutine 和用 Ruby 性能差别这么多,问题就出在这里,调用的方法不对。而不是什么别的。

dsh0416 #24 回复

我又不打算混在一起讨论~

huacnlee 分析 2017 年的 RubyGems 统计数据 提及了此话题。 02月21日 11:15
huacnlee 将本帖设为了精华贴。 03月03日 00:03

ruby 的瓶颈在并发

内存比 2.5.0 占用多了吗?

补充一条 Vladimir Makarov ("No Russian") 总结他演讲内容的文章 https://developers.redhat.com/blog/2018/03/22/ruby-3x3-performance-goal/

又是一篇回复堪比精华贴的帖子。话说…类似的帖子在社区里有不少,但是找出来很麻烦,新人进来也不晓得,也看不到。能否做个类似精华帖的聚合板块方便浏览?

jakit #21 回复

你最后说的那个,别说,还真有:facebook 的 BigPipe

https://molezzz.github.io/ruby/2014/07/12/bigpipe-with-ruby.html

ibcker #33 回复

嗯,这个在 HTTP 1.1+ 之后有了长连接,chucked 是可以做的,如果你不使用 Rails,垂直 Puma 直接编程应该可以

Rails dispatch 到特定控制器、特定方法的时候,假设是在 A 线程里面的,但是你开一个线程 B 去处理数据库读出来,如果不 join,A 可能已经返回给客户端了。

你说的 BigPipe 得改造 Rails 的响应方式。

在博文中,已经明显这样的处理了:

get '/chunked' do
  response['Transfer-Encoding'] = 'chunked'
  stream :keep_open do |out|
    out << erb(:bigpipe)
    sleep 2
    out << '<script>Bigpipe.puts("pagelat1","这是第一块")</script>'
    sleep 5
    out << '<script>Bigpipe.puts("pagelat2","这是第二块")</script>'
    out.close
  end
end

如果你非得讨论这个,你还可以上 HTTP/2 呢,HTTP/2 可以并行长连接,而且第一次握手之后的都是跟 WebSocket 类似的流,而且服务端和客户端还可以做互相的 RPC 反向推送,可以认为就没有谁是服务端谁是客户端的区别了。

HTTP/2 比 你说的这 BigPipe 本质上就是 HTTP 1.1 chucked 流先进多了去了

所以重新审题,我说的是 Rails 在 React 响应 Action 那里分配一个线程 task 处理 controller method 的业务

Rei #22 回复

现在共享内存还容易出 bug 么?用 Rails 写 web 的话没啥必要写入共享内存吧,除非是 Rails 或者 Puma 自己的 bug。

ksec GraalVM 1.0.0-rc1 released! 提及了此话题。 04月19日 17:11
jakit #13 回复

确实,这个 JIT 有点粗糙了,JVM 的 JIT,V8 的 JIT 等等这些 JIT 可是直接内存生成不依赖其他编译器 (GCC,LLVM 等),希望 Ruby 以后可以慢慢改进吧,毕竟人手有限,直接生成 JIT 没那么简单做出来

需要 登录 后方可回复, 如果你还没有账号请 注册新账号