YJIT: Building a New JIT Compiler for CRuby(shopify.engineering) |
YJIT: Building a New JIT Compiler for CRuby(shopify.engineering) |
But it is AOT! That means we could at least have some widely adopted and supported AOT solution.
The performance of Truffle is amazing when it works, but I think the compatability issue is going to hamstring it for a while.
Source: Computer Languages Shootout
Even for PHP the new VMs and speed improvements might be too late.
There was JS Engine performance war before Chrome v8 came around. From JavaScriptCore to whole bunch of monkeys [1] from Mozilla. Google v8 just makes the competition super heated and every one of them were working around the clock trying to out compete each other on the latest benchmarks. ( That was the dark era when Browser only cares about JS benchmarks scores and nothing in real world )
It would not be an exaggeration to say all the man hours on JS JIT is more than Perl, Ruby and PHP's VM combined. That is why I often said Ruby is the only Top 10 language that gets little to no funding and backing of FAANG. So both YJIT and Sorbet are much needed contribution from Stipe and Shopify. Along with help from Github and Gitlab ( hopefully :) ).
The VMs that gets more resources than JS would be JVM and .Net. And JVM is a monster on its own. Easily a multi billion dollar of investment over all these years. Or something using less man hours and resources like LuaJIT. But then Mike Pall is a super human.
[2] https://github.com/sagemathinc/JSage/tree/main/packages/jpyt...
I think one could radically change the way Python objects work internally, and have the C foreign function interface (FFI) wrap every object passed to a C extension in an API/ABI-preserving facade (which itself would wrap any objects returned from its methods). However, this would probably greatly slow down C extensions, which are often performance-critical sections of Python applications. It's also possible that there are portions of the C extension API that expose enough details of object internals to even make such facades herculean to implement. (I've only written some small simple C extensions and am not very familiar with the API.)
V8 didn't have to deal with API/ABI compatibility with any preexisting C extensions that may have made too many abstraction-violating assumptions about how objects and the VM worked.
Breaking too many important C extensions would almost certainly send Python the way of Perl 6.
Edit: as an aside, a big difficulty with JS is that objects can have their prototype changed arbitrarily at runtime. Even with Metaclass programming in Python, the class of an object can't be changed after creation, making it much easier to cache/memoize dynamic method dispatch. On the other hand, high performance implementation of Python's bound methods require a bit more flow analysis than you need in JS. In Python, if you write f = x.y, f is a "bound method" (a closure that ensure x is passed as "self" to y). It's expensive to create closures for each and every method invocation, so a high performance implementation would need to do a bit of static analysis to identify which method look-ups are used purely for invocation, and which look-ups need to create the closures because the method itself is passed around or stored in a variable.
Other languages struggle in this regard. Comparatively I imagine far fewer developers working full time on Ruby/Python, not to mention they would have budget constraints to hire and retain talent.
The are 2 main reasons for that.
1. For a good decade - from ~2005 to ~2015 - Ruby was among the most often used tech stacks at startups, and any performance work on Ruby (of which two major fronts were GC and JIT compilation) were perceived as extremely impactful and attractive.
2. Ruby is actually one of the most if not the most dynamic and complex programming languages out there and thus is one of the most challenging to build a runtime for and optimize. It became a de-facto benchmark for JIT research (among the more conventional Java). Over the years many vendors invested into Ruby compilers to push their underlying VM technology. Microsoft sponsored IronRuby to improve their .NET runtime, Sun sponsored JRuby and Oracle sponsors TruffleRuby.
As for progress, the biggest roadblock to Ruby JITs adoption has always being Rails. Rails uses a lot of Ruby features and pushes the language pretty far. Thus, you can't run Rails without your Ruby implementation being very complete and very MRI-compatible (MRI is the default Ruby implementation).
Plus, Rails uses Ruby in a way that is defeats virtually all best practices to produce a JIT-friendly code. Thus, there's no JIT compiler that offers any performance improvements for Rails apps - and in truth there might not be one ever. In fact, YJIT is exciting because it's the first JIT-compiler that seems to offer some speedups for at least a fey Rails benchmarks. People follow it closely, because these speedups might be a fluke, and as the compiler becomes more compliant, they may disappear (that happened in past with some JITs).
Other people tackle this problem by switching away from Rails to other frameworks. AFAIK Stripe themselves don't use Rails in most of their code, so they might benefit a lot from this work even without big improvements for common Ruby benchmarks (language shootout, Techempower, Discource benchmarks).
They're not. YJIT is really 100% compatible with regular interpreter MRI and already run a small % of production traffic at Shopify as well as fully pass the gigantic test suite of Shopify's 10+ years old monolith as well as GitHub's test suite.
This is not a fluke, that's what you get by building a JIT directly inside MRI rather than starting from scratch. It's harder and slower, but you get full compatibility from day 1.
Rails uses Ruby in a way that is defeats virtually
all best practices to produce a JIT-friendly code.
I've seen this mentioned a lot, and certainly the history of compiled Ruby + Rails benchmarks indicates this is true.However, I've never quite understood -- why exactly is this the case?
Is it just the sheer size of Rails? Or is it (ab)using Ruby in weird ways?
edit:
This is the more or less definitive answer, I guess, though it's a bit over my head!
https://k0kubun.medium.com/ruby-3-jit-can-make-rails-faster-...
I’m giving a talk about the history of compiling Ruby at RubyConf.
- 2 Rubinius attempts (Gnu Lightning and LLVM)
- MacRuby
- JRuby (do you count InDy separately?)
- IronRuby (do you count DLR separately? They didn't start with DLR, as far as I recall)
- MagLev - I think they hoped to GemStone would JIT user's Ruby code with bits of interpreter.
- TruffleRuby
- RubyOMR
- Vladimir's MJIT
- Koichi's MJIT
- YJIT
Do we count HotRuby and Opal? - both compile to JS.
I miss a few.
EDIT: mRuby, duh
I'm so happy for Maxime Chevalier-Boisvert to have such a resounding success with her JIT research after so many years!
HPy is building an API abstraction layer which is designed to be used with both the CPython API and JITs. However, IIUC they are not proposing any changes to CPython itself, but rather to provide a smaller API surface and fewer JIT impedance mismatches when extensions are built against something other than CPython. The lead developer is a longtime PyPy developer.
If you're interested, I'd be happy to take look anyways and see if there are any easy, idiomatic performance changes that can made for the Julia code without changing the algorithm.
MRI however implements some of them (integers, floats, symbols, true, false, nil, I might have forgotten one or two) using type tagged values instead of pointers to objects.
Type-tagging is easier to make fast for a highly gc'd dynamic language, as it reduces gc pressure substantially to not have to allocate lots of small objects without massive amounts of complex optimisations.
- DragonRuby - I don't know if it should count separately form MacRuby
- There were at least 5 Ruby-to-JavaScript compilers I remember from about 10 years ago, but they are very hard to find. Surprisingly, one of them - ruby2js - is still alive and actively developed!
While the loss of the Rails hype has certainly made things quieter in the Ruby community, for people like me who never liked Rails in the first place the reduced Rails hype has been a blessing, because it means I don't get asked about Rails every time I bring Ruby to the table anymore.
It's on Github [2] but note that the version on Github lacks a huge amount of changes sitting in my local tree that I haven't gotten around to cleaning up and pushing (including dependencies on how I've structured my local setup), and it'd be rough for anyone who isn't me and knows where the issues are. I'll eventually around to putting it into a somewhat more usable state for others to try...
A couple of "fun" aspects of it, though:
- It uses Rouge for syntax highlighting, and generally I've tried to rely as much as possible on gems rather than writing custom code (and I'm on a quest to split out whatever I can make generic enough into separate gems).
- It talks to a server process via Drb (going to change that for various reasons). The server process holds all the buffers, and snapshots them to a json file frequently. As a result every single file I've opened in Re the last several years - all 1600 of them - are retained as buffers and loaded into memory when I restart my laptop. I've not gotten around to adding a way to kill a buffer because they take up "only" 68MB total. The client-server approach meant I could switch to use it long before it stopped crashing, since as long as my changes don't corrupt the server-side buffers, it usually only crashes the client (even server-side exceptions gets passed along by Drb to the client)
- I rely on it calling out to a script to split panes, since I use a tiling vm (bspwm), so I have emacs like keybinding to split horizontally and vertically that uses bspc to control bspwm (I have a script for i3 as well) to split the pane accordingly and start another copy of Re that opens a view to the same serverside buffer. E.g. "split-vertical" just does "bspc node -p south ; exec #{cmd}" where "cmd" is a command line passed from re that will typically be <full path to re.rb> --buffer <numeric id of the buffer to open>.
- To open files or switch between buffers, or select themes, I use rofi rather than build a selector into Re. But it calls rofi by calling out to a script so anything that can take a list of buffers and open a dialog to select one will work (such as e.g. dmenu). E.g. here's the "select-buffer" script that is executed when I do ctrl x + b:
re --list-buffers | rofi -dmenu -filter `pwd`/ -p "buffer"
Part of the idea is to make the editor itself as minimal as possible, and farm out everything that can be farmed out to either separate tools or separate gems.Being able to do page interactions over Hotwire without Webpacker with be a real productivity boost for me.
Having stuff like YJIT and Sorbet Compiler available if needed only makes it more attractive.
I don't think anyone investing in this aims to restore peak Rails hype, so saying its too late to do that misses the point entirely.