Shopify开发团队公开以Rust重写Ruby YJIT的权衡与改进细节

Ruby 3.1初次加入程序内JIT编译器YJIT,经过一年的时间,YJIT成为Ruby 3.2中的正式功能,Shopify R&RI(Ruby & Rails Infrastructure)团队则是该功能的主要推手,开发团队现在详细说明YJIT的开发细节以及实质带来的改进。

由于Shopify的所有重要基础设施,包括商店以及网站的底层,都依赖Ruby和Ruby on Rails,Shopify的大型服务器集群传播全球,每分钟能够处理超过7,500万次请求,在2020年的时候,Shopify决定要开发一个相对简单的JIT编译器YJIT,目标是以YJIT提高店面渲染器(Storefront Renderer,SFR)的效率。

这原本只是Shopify在2020年启动的内部项目,但是受到CRuby核心贡献者的邀请,YJIT进入Ruby并且在3.1版本中作为实验性功能发布。YJIT编译器到了Ruby 3.2成为正式版本,比起Ruby 3.1的实验性版本,不只更强健也更易于维护,内存效率与执行性能的提升,使得YJIT 3.2更适合用于生产环境中。

Shopify在开发YJIT 3.2时,决定要将YJIT从C99移植到Rust上,开发团队提到这项决定的主要考量有两个,第一个是Rust提供了C所没有的安全性,开发团队认为,这对于具有许多约束的低端系统程序开发来说非常重要。

第二个原因则是Rust的程序代码更容易维护。使用C编写YJIT程序代码时,开发团队参考C宏实例动态数组,他们认为这样的方法既不安全也不够聪明,而Rust拥有丰富的标准函数库和好用的抽象,因此他们只花3个月就将YJIT移植到Rust,还使整体程序代码库更容易维护。

Ruby 3.2中的YJIT内存使用更有效率,开发团队解释,JIT编译器的缺点之一,便是需要使用比解释器更多的内存,因为JIT编译器需要生成可执行机器码,但是解释器不需要,而且JIT编译器还需要分配内存给元数据,因此也增加许多内存开销。

YJIT 3.1编译器大量的内存开销,使得Shopify在生产部署上面临挑战,因此开发团队进行多项改进大幅降低内存用量,除了优化元数据占用的内存,并且对不再使用的机器码实例垃圾回收,因此在Ruby 3.2中,YJIT内存开销已降到Ruby 3.1的三分之一,YJIT只会懒散地替机器码分配内存分页,而不会预先分配并且初始化一大块内存,这项改进让YJIT更适合用于生产中。

YJIT 3.2改进还包括执行速度,不只使用更少的内存,而且在Railsbench基准测试上,执行速度比Ruby 3.2的解释器快38%,与Ruby 3.1.3的解释器相比更是快了57%。

YJIT 3.2因为有一个新的后端,可以对多种CPU产生机器码,因此添加支持了ARM64 CPU。之前在Ruby 3.1中的YJIT只支持Mac和Linux上的x86-64,但因为Shopify开发人员开始采用Apple M1/M2笔记本,导致他们自己只能使用Rosetta模拟执行YJIT,这驱使他们开始提供ARM64 CPU的支持,使得YJIT可以在Apple M1/M2、AWS Graviton以及树莓派上执行,而且比起英特尔的x86-64 CPU,YJIT在Mac M1硬件上获得大幅加速。

因为YJIT 3.1内存开销未经优化,因此只标记为实验性,Shopify也未将其部署到全球,但通过部分SFR节点部署收集统计资料,观察到基准测试中无法发现的性能问题,而到了Ruby 3.2,YJIT已经获得足够改进,现在Shopify已经着手将YJIT 3.2部署到全球SFR基础设施,获得5%到10%的实际加速。

开发团队提到,目前YJIT最大的缺点,仍然是内存占用,开发团队还会继续减少内存占用,同时他们也将改进Ruby的执行速度,开发团队解释,Ruby中有许多方法调用,循环迭代和大部分的基本操作都是方法调用,典型的Ruby程序代码存在许多小方法调用,因此他们试图会从方法调用加速下手,改进Ruby程序执行速度。