RJIT, a new JIT for Ruby
github.com
github.com
In Smalltalk's case, it was its JIT that ended up powering Hotspot.
My greatest practical frustration from this is the difficulty of trying to claw back performance when it becomes important. I'd like more ways to say "from this point on, none of X, Y, or Z will change", and get some performance guarantees in return. For example, we have some code that dynamically generates a whole lot of classes from protobuf definitions. It's bad enough that it takes nearly a minute just to load an run all that code, but even after that I'm paying the cost of assuming that any of those definitions might change at any moment. So I have awful load times, and awful runtime performance.
I guess what I'm asking is: do you see a future where there is more explicit control afforded to people who want to pick their own tradeoffs without resorting to writing everything performance-sensitive in extensions written in C/Rust/whatever?
In Ruby: probably not. In general: yes. Julia is probably the closest we have to this today with it's gradual typing. I would like to see more of this (and I suspect we will at some point).
I think the problem is different: Ruby is severely underspecified and the only way to get good enough compatibility is to piggyback on the official implementation with its interpreter, gc, libraries, C interface, build tools, package management, etc. I also think, but this is just a hypothesis, that most programs use library code implemented in C almost all the time, precisely because Ruby is so slow. That means a JIT has to reimplement lots of library code in a way that makes it JITable/inlineable and that is a lot of work + it's hard to keep it exactly compatible.
It is hard to do that in a way that removes all the (probably very obvious) inefficiencies.
Python has the same problem + it used to have a JIT-hostile leadership. Ruby is quite friendly towards JITs.
An approach exists already in the present, and it's Stripe's Sorbet AOT compiler (https://github.com/sorbet/sorbet/tree/master/compiler).
In CL, classes can be redefined at runtime and the changes effect already instantiated instances! This is just an example, not the only dynamic feature of course.
Common Lisp has SBCL which generates very fast AOT native code despite the dynamic nature of the language, so I am not sure that being dynamic is a great excuse for being slow.
YARV (Ruby's VM) is already direct threaded (using computed gotos), so there's no dispatch loop to eliminate. YARV is a stack based virtual machine, and the machine code that YJIT generates writes temporary values to the VM stack. In other words, it always spills temporaries to memory. We're actively working on keeping things in registers rather than spilling.
Ruby programs tend to be extremely polymorphic. It's not uncommon to see call sites with hundreds of different classes (and now that we've implemented object shapes, hundreds of object shapes). YJIT is not currently splitting or inlining, so we unfortunately encounter megamorphic sites more frequently than we'd like.
I'm sure there's more stuff but I hope this helps!
I've seen different people mean different things by this, do you mean the IR is a list of bytecode handler addresses, and then the end of every handler is a load+indirect jump? Or is there also a dispatch table? In my experience the duplication of the dispatch sequence (i.e. no dispatch "loop") is worth 10-40% and then eliminating the dispatch table on top of that a bit more.
CPUs work hard to predict indirect branches these days, but the BTB is only so big. Getting rid of any indirect call or jump, regardless if that is through a dispatch table, is a big win, perhaps 2-3x, because CPUs have enormous reorder buffers now and can really load a ton of code if branch prediction is good, which it won't be for any large program with pervasive indirect jumps.
> it always spills temporaries to memory. We're actively working on keeping things in registers rather than spilling.
In my experience that can be a 2x-4x performance win.
> It's not uncommon to see call sites with hundreds of different classes
Sure, the question is always about the dynamic frequency of such call sites. What kind of ICs does YARV use? Are monomorphic calls inlined?
It's the former. Each bytecode is the handler address and every handler does a load + jump. There's no dispatch table (though there are compilation options that allow you to use a dispatch table, but I doubt anybody does that since you'd have to specifically opt in when you compile Ruby).
> Sure, the question is always about the dynamic frequency of such call sites. What kind of ICs does YARV use? Are monomorphic calls inlined?
In one of our production applications, the most popular inline cache sees over 300 different classes and ~600 shapes (this is only for instance variable reads, I haven't measured method calls yet but suspect it's similar).
The VM only has a monomorphic cache (YJIT generates polymorphic caches), and neither the VM nor the JIT do inlining right now.
What's the state-of-art in reg allocation? I see that the Android Runtime makes use of SSAs to allocate registers in linear-time [0]. Are other language runtimes pushing the boundaries further and in different ways?
Curious: What register allocation algorithm do the current Ruby JITs use? Is that influencing the work on this new JIT too?
It is easy to get a 10-20x speedup, if you limit yourself to compiling a subset of Ruby. When I first wrote Ludicrous JIT, I saw huge gains, but as I implemented more of the language, performance improvements over MRI (and later YARV) became more modest. Off the top of my head:
Implicit promotion from Fixnum to Bignum adds run-time type checking and overflow checking for integer math. This significantly cuts into performance on math-heavy benchmarks.
Ruby does not store Fixnums and Floats in their native representation. This means they must be converted from Ruby's internal representation when doing math.
Anything that uses eval prevents local variables from being optimized away, stored in registers, etc. The call to eval may lie outside the method being compiled. For example, a method may pass a block to another method. The other method may convert that block to a Proc, which it can use as a binding when calling eval (I don't know if Rails still uses this idiom, but this was one of the reasons why performance gains when running Rails on JRuby were more modest than otherwise might be expected).
Any program that uses set_trace_func can get a binding for any method invoked while the trace func is active. A Ruby implementation that supports set_trace_func is severely limited in what optimizations it can make. IIRC JRuby disables set_trace_func by default.
Exceptions in Ruby are implemented using setjmp/longjmp (or at least they used to be -- I haven't done low-level ruby programming in a long time). Other languages can use zero-cost exceptions without breaking backward compatibility.
YJIT is written in Rust, not C, but it's also not just a matter of wanting to write our own JIT for fun. There are a number of caveats with TruffleRuby which make a production deployment difficult:
1. The memory overhead is very large. Can be as much as 1-2GB IIRC.
2. The warm-up/compilation time is much too long (can be up to minutes for large applications). In practice this can mean that latency numbers go way up when you spin up your app. In the case of a server application, that can translate in lots of requests timing out.
3. It doesn't have 100% CRuby compatibility, so your code may not run out of the box.
There's a reason why you don't see that many TruffleRuby (or TrufflePython, TruffleJS, etc.) deployments in the wild. Peak execution speed after a lengthy warm-up is not the only metric that matters.
W.R.T. memory usage that's true, but I think they've been making big improvements there lately with things like node inlining so it may not be true in the near future.
W.R.T. 2, what are you comparing against here? Is the TruffleRuby interpreter that much slower than the CRuby interpreter, also once you use the native image version? Because it seems like once it starts compiling the hotspots the program must get faster than a purely interpreted version. Whilst it may take minutes to reach peak performance for that engine, how long does it take to reach the same performance as YJIT?
W.R.T. 3, yes, but is it easier to fix that or to develop a new JIT from scratch? What is your solution to the C extensions problem for example? My understanding is that this is a major limit to accelerating Python and Ruby without something like Sulong and its ability to inline across language boundaries.
In my experience, Ruby is not super slow.
In my machine, I can create 1M of empty hashs on 0.17sec.
Benchmark.measure{1000000.times{Hash.new}}
@total = 0.1706789999999998s
It's very good for a dynamic language.But ActiveRecord (and Rails) are incredibly slow.
In my machine in 0.17sec only 2000 Models can be created.
Benchmark.measure{2000.times{User.new}}
@total = @total=0.17733399999999833.
Some SQL+Network runs in less than 10ms, in these cases Active Models creation is slower than that.Yes, Rails can be slower than database access.
I wouldn't pick it for a new project though if I could write Rust or Elixir or Crystal etc unless they lack something in their ecosystem that Rails has, but over time this will become less of a difference.
If so, that probably is more a benchmark of your memory allocator, which probably is written in C than of ruby.
I also guess you ran the benchmark from a new ruby instance. That means memory wasn’t fragmented. That certainly doesn’t make the allocator’s job more difficult.
And anything in Rails is slow, from templates to ActiveJob.
IMO, it doesn’t. Your first benchmark mostly hits your memory allocator, the second exercises both ruby and ActiveRecord (a component of Rails), and, I guess, does more (depending on the complexity of your User class)
That it takes more time may be because ruby is slow, because ActiveRecord is slow, or because of it doing more. It probably is a bit of each, and may be largely because ActiveRecord is slow, but that benchmark doesn’t tell us that.
A fairer benchmark would create a pure ruby class similar to your User class, populate it similar to what ActiveRecord does, and compare timings.
Pointing out an item a lot higher up on that list that’s also written in ruby and about as easy to use would be a way to support your claim.
I'm on an 8 year old i5 3.2ghz CPU based workstation running in WSL 2 with Docker.
With Rails 7.0.4 and Ruby 3.2.1 (YJIT not enabled):
1M empty hashes:
Benchmark.measure{1000000.times{Hash.new}}
@total=0.192643
But in 0.192 seconds I'm able to make 12,500 user models: Benchmark.measure{12500.times{User.new}}
@total=0.19225799999999982
That is over 6,000 more than your ratio but based on your first number something tells me you have a much faster dev box than me.A better benchmark https://www.techempower.com/benchmarks/#section=data-r21&tes...
1. drogon (C++) 92.3
2. actix (Rust) 90.7
3. asp.net core (C#) 83.7
~~ mind the gap ~~
4. gin (Go) 23.0
5. spring (Java) 21.8
6. phoenix (Elixir) 8.1
7. express (Nodejs) 7.3
8. laraval (PHP) 4.4
9. rails (Ruby) 4.3
10. django (Python) 3.2
https://www.techempower.com/benchmarks/#section=data-r21&tes...I moved from C# to Ruby, and along the way I read Design Patterns in Ruby[1] (which I highly recommend), and it went through several of the patterns I used to have to apply in C# to make it workable, and it would then show me they weren't needed in Ruby because of the structure of the language. Maybe C# has improved in these kind of things, but things like that matter when developers are also expensive and you want to crank out features.
The hosting question has me a little confused I guess. You don't need anything special to deploy or run modern .Net code? You can run your app anywhere that the Asp.Net Core runtime is supported [2]. Or bake your app into a container and run it anywhere that understands containers. MS has tutorials for deploying to Azure [3], but I honestly know nothing about it (Azure). Every place I've worked at that had cloud infrastructure has used AWS, and I've never needed to deploy a hobby app.
0: https://learn.microsoft.com/en-us/aspnet/core/tutorials/min-...
1: https://learn.microsoft.com/en-us/aspnet/core/tutorials/choo...
2: https://dotnet.microsoft.com/en-us/download/dotnet/7.0
3: https://learn.microsoft.com/en-us/azure/app-service/quicksta...
I'm not sure how much you've played with Node, Ruby, etc BE ecosystems but there are plenty of "just `git push` and watch you app automatically deploy" services out there that makes it really easy to get started playing around with the actual framework and not get bogged down in docker files or AWS configuration.. Maybe I'm spoiled, but I'm also... busy. And I'd like to be able to try out a framework first before I decide if I wanna invest a lot of time working on a full-fledged project with it and these sorts of tools are perfect for that
Thanks for the links
Edit: Assuming a modern framework that can run basically anywhere. Something like old school .Net Framework that is essentially restricted to Windows and IIS would be a different story.
When it does get migrating, surprising as it may be, many companies are more willing to move to another platform than keep it on .NET, as full rewrite is almost required anyway. Dependencies on missing APIs, COM wrappers and such.
It is quite different to use an ecosystem born and raised on UNIX, and one that only in the last 5 years kind of matured into UNIX.
Additionally there is the whole issue with politics in VS4Mac/VSCode versus VS for tooling, which makes anyone outside of Windows to shell out for Rider if we want the same level of tooling.
Btw, regarding Postgres support: the guy maintaining the Postgres .NET drivers works for Microsoft on Entity Framework Core.
Regarding the boilerplate and the minimal hosting model and the minimal api… it starts to look like a nodejs express setup. Personally I think boilerplate was not that bad to begin with and saving a couple of lines of code (sometimes at the cost of legibility) is not worth it for me in a decent sized project.
Not sure if either of those points matter 8+ yrs on since I last looked, but I know I'm not the only developer who has these concepts burned into their soul.
Genuinely curious if anyone out there has some wisdom to share about this since I know next to nothing about the ecosystem
For this site's audience, I would recommend taking a long look at F# over C#. If you aren't aware of it, it's functional-first, OCaml-inspired, but you have access to all the Asp.net core framework you need (similar to accessing Java from Clojure). A batteries-included framework that seems popular is the "SAFE Stack"[0].
I'm a Rails, Django guy and we run into exactly what you're talking about. The speed we can pump out features is fantastic in both, but once you get to a large number of objects, it's better to drop into pure sql/array handling.
This is mostly for data processing, background jobs where we need to work with >1k separate objects.
Now please note, this only talks about what Microsoft is doing, maybe others do the same.
Edit: I saw the article does in fact look at other frameworks.
In the end, it just means these particular benchmarks are pretty much worthless for any kind of real-life comparison.
It's interesting because even the maker of actix (Rust) was pretty explicit about their goal to optimize for these benchmarks. And it really worked to improve the perception of Rust for web dev applications.[0] The current leader is actually JUST which is a JavaScript based framework that's pretty much solely focused on these benchmarks.
I think the strategy for optimizing like this and sometimes even fudging numbers a bit is pretty standard for a new framework or platform trying to make a name for itself (ahembunahem).
Actix is new, JUST is more of an experiment, and tbh I've never even heard of drogon. But when it comes to frameworks like ASP.NET core, Laraval, Rails, Express, etc you can at least trust that they've been around for a long time and there are many proven examples of their application to both commercial and hobby projects. IMO the fact that ASP.NET has been around for so long yet can still top benchmarks like these is still a noteworthy feat. And its undeniable that Django, Rails, Laraval, etc are leagues behind in terms of performance
Hmm no:
„the expensive Go implementation ranks 22nd overall in the TechEmpower Fortunes Benchmark with an equally impressive 381k requests/sec. Not quite as fast as the Java one but still more than 2x faster than the equivalent test in ASP.NET Core.“
But as said, in the end the whole discussion probably says more about the value of this benchmark than the value of the frameworks (also considering of course that pure performance is only one factor among many)
It makes for a really interesting value proposition for the benchmark.
Also, Sequel exists. Yes it’s not the Rails way but is fantastic.
If ActiveRecord is affecting code speed I use raw sql which returns an array of hashes. And if the application needs to be fast, using ruby, the best way is to use Rack directly without frameworks.
Btw one project I work on switched to YJIT in production and there are no problems so far (but no noticeable perf gains either)
1. Kokubun, who works with us on the YJIT team, is leveraging insights he's learned while working on YJIT to build this. He has said so in his tweets, some of the code in RJIT is a direct port of the YJIT's code to Ruby. This is his side-project.
2. One of the challenges we face in YJIT is memory overhead. It's something we've been working hard to minimize. Programming RJIT in Ruby is going to make this problems worse. Not only will it be hard to be more memory-efficient, you're going to increase the workload of the Ruby GC (which is also working to GC your app's data).
3. Warm-up time will also be worse due to Ruby's performance not being on par with Rust. This doesn't matter much for smaller benchmarks, but for anything resembling a production deployment, it will.
On your second point, if you're not seeing perf gains with YJIT, we'd be curious to see a profile of your application, and the output of running with `--yjit-stats`. You can file an issue here to give us your feedback, and help us make YJIT better: https://github.com/Shopify/yjit/issues
Or it could be a win. Java went that way too.
Another meta-circular JIT implementation, instead of going the tried and through path that most keep on trailing.
Looking forward how it evolves from there.
https://ankane.org/new-ml-gems
But I don't know how often this is used in production. I've ended up training the models in python and then loading them in Ruby.
I'm still scratching my head, other than accessibility, Why Ruby over Rust.
Note: I'm a Ruby dev, I don't know Rust. I've written few toy interpreters in Elixir and OCaml. This is my very limited understanding of compiler design, etc.
You could say that about any language.
Rust's ADT seem particularly useful in this context. it really makes refactoring a breeze. (OCaml has a similar type system.)
Your point is generally true though.
I don't know if that's true or not, but I imagine most compiler engineers tend to be more engrossed in languages like *ML, Rust, or Haskell. Or languages that are common in general, like C++ or Python. Ruby isn't that popular (outside of the Rails niche at least?), and it doesn't fit very well in a compiler niche either, I think.
I write Crystal code and the fact that the Crystal compiler is written in Crystal allows me to make my code better/faster by taking a peak into the compiler code occasionally to see how things are done without having to get good at another language.
I am a huge Rust fan, but going up stack and writing your jit as first party is pretty cool. Maybe after RJIT is well factored, the internals could done in Rust again.
Really happy for Ruby!
They actually had a new release at the beginning of March.
Also how slow is still ok? People can talk about things that aren't 'performance sensitive' but at some point it's going to matter. If a program is serving up web pages, that's an interactive application and people are waiting on the program.
It's all about cost, really. But you can just tell Nginx to cache pages and then it's not a problem for the vast majority of use cases.
If JavaScript really isn’t fast enough, then I think you should be thinking about something like Go or Rust instead.
LuaJIT is an incredible piece of technology, but in this performance tier JavaScript has won out due to sheer ubiquity and the size of its ecosystem.
Edit: I never said slow was okay. I’m not advocating for slow, I’m just saying that the target audience for RJIT is developers who are already using Ruby. For significantly better speed, you probably want to look elsewhere.
Also how slow is still ok? People can talk about things
that aren't 'performance sensitive' but at some point
it's going to matter
Done a fair amount of Rails perf tuning over the years. One of my favorite things to work on."Fast enough" for me, is when your web framework is nowhere near your bottleneck. On your average web app endpoint you're probably spending 95-99% of your time on external calls to Redis/Postgres/etc and Postgres is probably your specific bottleneck.
For these apps, Rails is most definitely fast enough. You could rewrite your app layer in well-tuned C or assembly and guess what, it's getting maybe 1-5% faster if you're lucky. Maybe you get from 100ms down to 95ms and all you had to do was rewrite 100,000 lines of Ruby in 200,000 lines of C.
For other cases, obviously, maybe Rails is your bottleneck. Maybe you're providing a read-only API and everything is cachable in RAM. Rails will be fast, maybe 10ms per request, but a faster framework can spew out responses in 2ms and now you have 5x the capacity and your P95s during peak hours are really smoothed out.
But most of the Rails app, especially those with ActiveRecord and Server side rendering spend 60+% of response time, And in many cases even higher before hitting DB. And they fall into 100 to 200ms response time category.
Impressive
According to the computer language shootout all micro-optimizations
*edit, fix confusing vernacular
So yes! That is great news.
Edit: I guess this (https://github.com/ruby/ruby/pull/5944) PR was merged.
Robe runs inside a Ruby process, like Common Lisp's SLIME. This is (far) superior to typical IDEs because it's no longer restricted to static analysis. In a highly dynamic language like Ruby, static analysis can do only so much, so RubyMine and LSP servers have to guess data types. Robe doesn't have to.
Inf-ruby allows interactive evaluation of code, even including redefinition of methods & classes. The items being redefined don't have to be at the top-level, since inf-ruby automatically extracts the surrounding method and class definitions. This means you can just open any source file of your program, edit any method/class that may or may not be deep inside layers of modules & classes, and just ask inf-ruby to send the edited definition to the running Ruby process. Instant code reload.
Of course, it's still Emacs, so sharp corners and rough edges abound. But when it works, it's a boon.
Ill try the Emacs setup.
Rust has got to be the ugliest, most unfriendly programming language I've ever laid my eyes on. And I wrote Perl for 10+ years, so that's really quite a feat of aesthetics failure.
Anyhow, I'm pretty impressed with the performance thus far, I like the idea of having multiple JITs available for a single-language ecosystem, regardless of how disgusting the language used to implement them. I think having competition means that there will be a race to the bottom and towards the "center" of general work. It's already really cool to see how the different approaches have clear preferences of the tasks they excel at and where they fall short.
This is hugely valuable because it pushes Ruby forward for everybody, and will hopefully result in not only a faster Ruby for X, but a faster Ruby for everything, which is just an objectively good thing.
Python is in a weird spot in this arena, because it is very clearly and very strongly orienting itself to continue to dominate practical data science work, and that means the need for JITs to handle regular jobs like text munging and whatnot fall by the side in order for the latest NumPy and Jax stuff, whatever is the current hot shit in the AIverse. Ruby doesn't suffer from that because it's pretty solidly lodged in the web development sphere, while also having a capable presence in netsec tools, application scripting, and probably a few more areas that I'm not aware of.
If you're interested in some of cutting edge Python stuff, I'd recommend taking a look at exaloop/Codon. Codon will soon be able to output Python extensions that are compatible with Python's setuptools, so it will soon be possible to just include some .codon files with your project, use setup.py, and have decorators that can (literally) 100x your hot loops.
No, that would be C++ or PHP.
I've written Ruby for 10 years. I can't wait for a future in which my team never needs to worry about mutated shared state. Rust is beautiful in this regard. The only languages that come close are Clojure and Haskell, but Rust accomplishes this without immutability or a bloated runtime.
I think the only thing Rust has really brought to the table is the idea that alternative memory schemes like ownership can be valuable. That idea will slowly disseminate into Python/Ruby, and other languages from there, and those of us who appreciate beauty will continue to program with proper tools.
I'm actually looking into Ocaml lately. The 5.0 release is really interesting, and the next release of OPAM will have proper Windows support and everything. Rust takes a lot from Ocaml, as I understand the original compiler was written in it, and inspired by it.
I think mutable shared state is bad, as you do, but I think so less because of the advantage that a tool like Rust or Erlang brings to the table in handling that state, and moreso because of the programming practices it brings about regardless of the language. Treating a session as stateful, or really treating any io-capable object as mutable is definitely a big mistake. That much is clear. I think as the years go on, more languages will embrace this wisdom and replace Rust's incredibly rigid and unfriendly experience with things like proper object capability systems, effect handling, and whatever instruments would be useful from formal verification methodology.
And again, all of that could conceivably land in Python/Ruby eventually, or as extensions, whatever.
I guess what I'm saying is that over a long enough timespan, it would seem that ideas are always going to win out and language designers will slowly advance towards that front, while retaining beautiful code.
It's interesting that you mention Clojure, because a lot of really cool ideas bouncing around the Ruby/Python sphere originated or at least came to widespread popularity in Clojure. I'd put Erlang up there as equally influential as well. Clojure also shares the feature of highly-writable and highly-readable code, which qualifies it for beautiful, imo. How often do you come across a Clojure function that's just cluttered with {}{}<T><T><<T>RefCell>(); bullshit, to the point where it's not immediately clear what is happening? Now, compare that with Clojure. If ever there's a complicated Clojure function, I'd wager that it either a) deals with Java interop, which, God bless you. or b) is just a highly abstracted function, and can be grokked by walking backwards up the calls one-by-one. Terseness can be daunting, but it's almost always a simple case of gathering context to understand those functions ending in )))))})).
Anyhow, I've wasted too many bytes here.
Since you've a decade in Ruby, have you tried Crystal at all? I heard about it last year sometime, but was turned off by no Windows support.
Even everyday decisions like whether to copy an array passed to a constructor can be fraught with danger. Is the caller going to retain a reference and mutate the array after I check invariants? This simple yet intractable problem lurks in every imperative language except Rust.
Channels in Go are another great example. CSP is a sound approach for safe concurrency, yet the Go language offers no assurance that references passed over a channel are exclusive to the receiving goroutine.
I don't see how languages like Ruby or Python or Go could incrementally add a borrow checker. It would break too much. Returning to other programming languages after internalizing Rust compiler warnings is really eye-opening.
I have not tried Crystal. I also haven't written OCaml, though I did dabble in F#. Rust's match expression was definitely inspired by OCaml.