Ruby JIT for MRI 开始开发了

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

来自这个 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 回复

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

 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): [email protected]: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 回复

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 回复

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

dsh0416 回复

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

dsh0416 回复

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

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

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

pynix 回复

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

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

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

jakit 回复

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

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

jakit 回复

我期待它能本身提供编译的功能,就是 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 回复

为什么 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 回复

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

jakit 回复

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

Rei 回复

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

Rei 回复

等待数据库处理的时间是数据库本身的问题了,不属于 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 回复

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

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

Rei 回复

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

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

jakit 回复

「让一个性能很好的 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 回复

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

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 回复

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

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

ibcker 回复

嗯,这个在 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 回复

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

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

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

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