Ruby 3.2’s YJIT is Production-Ready
shopify.engineering
shopify.engineering
This means that you often have to deal with, say 512 MB per instance, and then if using something like Puma, have to work out how to split concurrency vs memory footprint. What I'm finding is that v3.2 YJIT loves memory, so I have to trade off that, which means less concurrency per process. Benchmarking it quickly, the 15% gains I might get on the thread aren't worth having to move to just 2 threads on a 512 MB instance versus the 3 threads I can get with YJIT not enabled.
I think it's really neat and will continue to track it, but the performance in terms of memory trade-off isn't there quite as yet for our app profile. Not sure if others will find the same, but I guess its if their production environments aren't PAAS's with low memory headroom or not.
I'm mostly done with Heroku. With one kinda big app left, all other envs, and projects are now just on aws without the tax of heroku.
And I was a big heroku fan, but their recent decisions made me shop around. I do miss the price $$ of metal in a dc, but not the price :clock: of metal.
It might not be as nice as having the original package, but it is good enough approximation while using boring technology (TM).
I leave the cool languages for hobby coding.
PS: Any Microsoft people here? Ruby could be 10x faster on Windows, if we figure out how to load files faster! https://bugs.ruby-lang.org/issues/19325#note-11
Unless the result wouldn't be meant to be read nor understood by anyone.
IMHO Python is the closest to pseudo-code specifications.
Rather troubling experience are those bad days with your current real one, and you suddenly remember those sun kissed, beautiful days with your dearest ruby :D
I recently wrote a toy compiler for x86_64. I use an external assembler.
My dream is to write a JIT compiler runtime.
https://GitHub.com/samsquire/compiler
My understanding is that you generate machine code and then mprotect the code to be executable then jump to the void * as a function pointer to execute generated instructions.
What I would like to understand more is tracing compilers and how they optimise hot loops. And how they deoptimise and optimise at runtime based on runtime information.
Do you have any resources you would recommend to someone who want's to explore (or develop as you have) in this subject?
I am a beginner at assembly and learned everything from GCC.
There's so many details. I learned about rbp+rsp registers recently. And from Reddit post on the compiler someone told me I need to keep the stack aligned.
So if you are to make Ruby ~10% faster, you might reduce Basecamp hosting bill by 2% at best.
And so Ruby may scale in cpu performance, but not with your number of teams.
That’s why Spotify switched to Java (I think they were a heavy Python user before that, not Ruby, but not sure anymore - but the organisational problem is the same for those two).
Note that I still choose Rails for all my projects, but my company is small.
Such as?
I'd guess that the idea parent is trying to convey is that for a given size of a Ruby codebase the engineering organization needs to be structured like successful cases like Github, Shopify or whatever, but I'd be interested in the technical aspect to this. I agree with this idea when reasoning about microservices, but this is the first time I've seen it applied to the choice of language. It's intriguing.
And the benchmark game is using Ruby 3.1, whereas 3.2 is significantly faster. YMMV though, it is going to depend on your use case, but we are always working on making Ruby faster, and if you run into a use case where Python is a lot faster, you can ping me on twitter @Love2Code and tell me about it. We'll take a look.
[1]: https://docs.python.org/3/whatsnew/3.11.html#whatsnew311-faster-cpythonYJIT 3.1 was eagerly allocating it's executable memory region regardless of whether it was needed or not. With the default settings that meant an extra 256M of RSS right at boot.
As mentioned in TFA that's no longer the case in 3.2, so expect this RSS number to go down by at least a good 200MB once they upgrade to 3.2.
Python 3.11.1
ruby 3.2.0 +YJIT
I'm not sure where this leaves TruffleRuby itself, or plans to implement it at Shopify.
(Rest in peace, Chris, and may your memory be a blessing to those who knew you)
Here was the announcement:
Tl;Dr it runs rails for a while already, it even runs mastodon.
Also Oracle.
In what way? Truffleruby passes over 97% of CRuby's specs and it runs on my command-line without any problems so far. It was easy to install too. I'm sure if I dig into those failing specs I'll find something but will it be important? I'd love to know.
> Regarding performance, TruffleRuby is by far the fastest Ruby implementation on the yjit-bench benchmark suite which includes railsbench, etc. To achieve this performance TruffleRuby needs a fair amount of warmup, as other advanced JIT compilers do. If you find any performance issue, please see this guide.
One benefit of YJIT + cruby is it doesn't have the same warmup costs. If you're deploying many times a day, this JIT warmup becomes a dominant factor.
Very little of Graal/Truffle is locked behind an oracle license. The community edition is FOSS and within 5-10% of the performance of the paid version (so still an order of magnitude above MRI with or without YJIT). The situation is very similar to HotSpot/OpenJDK.
Source: I work in a team that's nearby as the org chart flies.
[0]: https://eregon.me/blog/2022/01/06/benchmarking-cruby-mjit-yj... says
Also our experience with truffleruby is death by a thousand subtle differences. It might be 97% compatible but chasing down that 3% undocumented behavior difference on every minor version update for every gem got exhausting fast. Everyone uses and tests against cruby.
I've never seen that credibly claimed. Why would their license ban benchmarking if their performance was any good?
By all means, run benchmarks on your workload profile and choose the technology that works best for you - that is perfectly OK. But not make claim that xxx beats oracle in yyy.
Oracle goal here is to make a good enterprise ready DB solution; not to fight the open-source community on hundreds of different performance claims, half of which have been specifically engineered to put the competing, leading product in a bad light.
You might not like what oracle does or how they do it, but it’s clear that they have a target on their back and sometimes the best way to play the game is not to play at all.
Just look at the « independent and fair review » that Top Gear made about Tesla.
Maybe. Or maybe it's to soak naive clients for all they're worth. The remedy for bad benchmarks is good benchmarks, not no benchmarks.
> Just look at the « independent and fair review » that Top Gear made about Tesla.
What about it? It was accurate and informative (unless you think Tesla lied about their range stats). The good points about electric cars are real but they don't mean we should ignore the downsides.
The car did inform them it would run out of battery on the way. They made a segment about it and failed to inform the viewer that they had ignored several warnings given by the car before reaching the « oh no we are out of battery in the middle of nowhere » point.
This is not what I would qualify as a « fair review ». There are no « false claims » (they did run out of battery, after all !) but the information are given and presented in a way that paints the reviewed product in a specific light.
The comment you replied to just said Oracle allows running benchmarks internally, though.
There are still quite a few enterprise workloads that only the likes of stuff like DB2 are a match for them.
HN has been host to quite a few of these spats over the years and I wouldn’t want to deal with it, there are pretty much only downsides to letting people poorly benchmark your product and have every incentive to get it wrong.
This was based on my conversations with Chris Seaton before his passing, about 6 months ago.
We're using Rails as API and web frontend with a pretty high number of requests per second, so I was hoping we'd see something. Does anyone have any experience rolling it out?
Does anyone have any experience rolling it out?
FWIW, the linked article has prod benchmarks. I'm pretty sure (there isn't a lot of info on how
to enable it in production workloads) we're running
YJIT in production
Sure you're running it? You have to compile with YJIT support, and then pass the command line arg. (It doesn't support YJIT out of the box because, I presume, they didn't want to force a Rust dependency on everybody?)Here's how on MacOS + asdf, others will be similar. Note that I think --enable-yjit might work (but do nothing) even if you don't have support compiled in.
# if you already installed 3.2.0
asdf uninstall ruby 3.2.0
# install Rust
asdf plugin-add rust
asdf...
# now install Ruby 3.2
asdf plugin-update ruby
export RUBY_CONFIGURE_OPTS=--enable-yjit
asdf install ruby 3.2.0
asdf global ruby 3.2.0
# verify it's installed and ready
ruby --enable-yjit --version
# now run your workload
ruby --enable-yjit foo.rb1. Compiled with jyjit confrmed (ruby --yjit) returns the correct value
2. RubyVM::YJIT.enabled? returned true
But the information available on how to confirm YJIT is running is not super clear. Since I didn't notice any improvements I started wondering.
Since I didn't notice any improvements I started wondering.
It only speeds up Ruby itself. In a "real world" web app performance, performance is mostly dominated by Ruby/PHP/Java/whatever sitting around and waiting for database queries to execute.So for a lot of web workloads the perf increase won't be huge. If your average endpoint is e.g. 5ms of Ruby and 500ms of DB queries, your max speedup will be negligible if you switch to YJIT... or even if you rewrite the whole app in hand-optimized assembler.
(Also gets to the root of why perf-based criticism of Ruby is usually dumb, IMHO. It's usually not the problem)
The best way to know it to enable YJIT runtime statistics [0] to see whether YJIT is doing a lot of side exits and if so why. But it requires a bit of YJIT knowledge to interpret them.
It's also entirely possible that your application isn't CPU bound in the first place, or that it's bottlenecked by things YJIT can't do anything about (e.g. GVL).
That's close to impossible to guess what it might be without being able to instrument the application.
[0] https://github.com/ruby/ruby/blob/df6b72b8ff7af16a56fa48f3b4...
The YJIT is not on by default in ruby 3.2, you have to specifically enable it. If you aren't sure if you have enabled it... what makes you pretty sure you have enabled it? It seems possible you have not enabled it, if you aren't confident you know how to do so?
I am not using it yet myself, and don't want to put any possibly incorrect info here about how to enable it. I agree that it's not clear to me where to find this documented. I am pretty sure it is not on by default.
GitHub issue link here: https://github.com/docker-library/ruby/pull/398
Initially when 3.2 was released Debian based images didn't support it because Bullseye ships with a version of rustc that's too old to compile YJIT.
But https://github.com/docker-library/ruby/commit/6db728e addressed this a few days ago by installing a pre-compiled version of rustc straight from Rust and YJIT is available in Debian now.
You can confirm if it's available to use by running:
$ docker container run --rm ruby:3.2.0-slim-bullseye ruby --enable-yjit -v
ruby 3.2.0 (2022-12-25 revision a528908271) +YJIT [x86_64-linux]
If it weren't available then you would get an unknown argument error. $ docker container run --rm ruby:3.2.0-slim-bullseye ruby --enable-yjit -v
ruby 3.2.0 (2022-12-25 revision a528908271) +YJIT [aarch64-linux]
Runs on Arm as well! $ podman run -it -e RUBYOPT="--yjit" ruby:3.2
irb(main):001:0> puts RUBY_DESCRIPTION
ruby 3.2.0 (2022-12-25 revision a528908271) +YJIT [aarch64-linux]
=> nil1. Compiled with jyjit confrmed (ruby --yjit) returns the correct value
2. RubyVM::YJIT.enabled? returned true
I didn't notice any improvements at basically so I can't really say _for sure_ if it was working as intended.
> For Rails you'd want to just edit the executable Rails scripts like `bin/rails` to include that flag.
Alternately, you can edit your shell init scripts to put the correct thing in `RUBY_OPTS` ENV variable, yes?
I would consider this preferable to editing executable rails scripts with implementation-specific stuff (and then hoping that the app is always launched with those edited shell scripts, which I'm not sure eg `bundle exec rails` will do). I don't think editing Rails scripts is probably the "right" or recommended way to do this.
But I'm not going to try to specify what you put in `RUBY_OPTS` since i haven't done it myself and don't want to spread misinformation! (Again, is this documented in a centralized trustworthy place? As I try to google it... I am somewhat frustrated with the ruby documentation situation!)
The performance impact of enabling yjit was quite obvious in our charts. Another giveaway that it's properly enabled is the servers will use consume more memory.
passenger_env_var RUBY_YJIT_ENABLE 1;
Ruby doesn't have that and adding it in poses problems for REPL development. They chose to go with a separate file for types. Which gets around the breaking change problem but the ergonomics are terrible. Think the hope is that IDE support eventually gets around that issue. But thus far not a whole lot of adoption.
Some of that can probably pinned on the greater dynamism of Ruby, making all manner of static analysis quite challenging.
But from what I've seen, it seems like the main reason is indifference or even mild hostility from the language maintainers. It looks a lot like they saw Sorbet taking off and slapped together a competing spec (without any associated tooling) that would ensure all type information stays outside of the `.rb` file.
It seems like the only impact it has had is to dissuade people who would like some static types from adopting it, in favor of this vaporware. Contrast this with Python's embrace gradual extension of mypy type hints.
I'd love to be proven wrong, that there are thriving projects building atop RBS. I'd still _prefer_ a system that allowed me to type a function signature so that anyone inspecting it could immediately know its shape, but at least the shadow types would be _something_.
My take here is that typed-ruby is something SOME people want (I'm one), but a lot don't.
I'm not sure what level of force that provides to anyone who has either tried or is considering adding it to the language.
Sorbet also provides a gradual typing system where you can start adding types to a large code base and gradually make the checking more strict. Stripe claims to be using it on a huge ruby code base and that their developers generally like it.
However I think many devs don't want static types on Ruby. We like our duck typing!
If a method expects to be passed an argument that it can call "quack" on, can't you define a type that has "quack"? The actual argument could be a Duck or a Goose but as long as it has a "quack" method the type will be OK
I haven't used types in Ruby (or much at all other than dabbling in Typescript), so I might be missing something
Spoiler alert: not a lot of fans
You know the yjit folks are going to take this and make yjit even faster right?
In Ruby, tests are the same you'd write in any other language. You test that things do what they are supposed to do, not the types of parameters or return values.
It's been fascinating following Maxime working on Yjit.
See Natalie [0] for a work-in-progress Ruby compiler which works ahead of time. It’s a super cool project but it’s just for fun [1] so I don’t believe it’s trying to compete on performance.
That means there is no need to write a fully-featured Ruby compiler, instead you only have to emit native code in places/situations where it will have the maximum benefit.
If it takes 1/2 the time to do something, you can do double the work in that time.
If on the other hand, you can do 1.5x the work in the same time, it means you've made it use 33% less time than before.
With 10% less time you're taking 1/0.9 = 1.11111x more work in the same time.
Needing 10% less time for the same amount of work means you can do 11(.1)% more work in the same time.
It took sometime to me too.
I first time learn Ruby from zero to "hero" in production confidently is in just under a week. And as far as i know, no other language could bring me such experience.
You get objects you don't know what's in there, 6 month later someone changed it, no compile error but it will break when you run it.
And so to overcome those major issues they added really ugly stuff that is not core to the language, linters, annotations etc ...
But static typing languages, library stacks, and environments have come a long way in the last 30 years. Now I prefer even to "prototype" in static languages because the crossover to when they start providing me net benefit is around a week or so into my development process.
I will not claim one is completely better than the other, but the balance is a lot more even than I would say it was ~20 years ago.
I've moved from Ruby to Crystal as much as I can, and at first I found it painful but now I appreciate the myriad complaints from the type checker. It's eliminated 99% of the specs I used to have to write.
I learned programming in the late 1990s. Looking at Windows programming at the time looking like gibberish to me and I looked forward to the day I would understand it. Honestly, it still looks like gibberish to me. I understand the whys and the wherefores better now and if I had no choice, yeah, I could learn it and write it, but the code from that still reads absolutely awfully to me, just unbelievably bad by almost every modern standard. Even when you're writing approximately equivalent code in C# at the same low level of acquiring handles and manipulating them with very low level APIs, it's just so much better now.
But in truth, you're right. Very few people know/cared about type inference at the time.
Optional typing, at least in TypeScript, will not give you the guarantees of type checking. It finds many of your type errors, but not all of them because the type system is deliberately unsound.
All you really needed is:
for (auto it : myMap)
and you still get compile type safetyIf you write code the way you write statically typed code, then sure, you're in for a world of hurt. I could imagine using a statically typed language again if it made me as productive as Ruby, but most statically types languages have near useless type systems that I wouldn't wish on my worst enemy.
Beyond that, it also forces me to think structurally from the start. But I tend to be working on rather complex applications, I almost certainly wouldn't use a static lang for data processing or similar tasks.
> It feels a lot like the people who used to try to convince everyone functional programming was the only way to go and object-oriented programming was for dinosaurs.
What do you mean "used to"?? Haha
However, once you have strongly + statically typed languages, you can very often create safe-by-design (tm) APIs, APIs that are simply impossible to misuse (for some definition of misusing). A trivial example (which among industrial languages works only in Rust and perhaps Ada) is a file system API in which the compiler detects that, in some codepaths, you're attempting to try to write to a file after closing it.
Exactly how much value this has for your programming depends on how many invariants you need to guarantee. If you're writing (for instance) a CRUD for non-critical data, that's usually not many. If you're writing a network protocol or a garbage collector, this can save you from worlds of pain. I have had to fix data loss and privacy issues in Firefox that would have been detected years earlier if we had used a strongly-typed language such as TypeScript (which didn't exist at the time) instead of JavaScript for the front-end.
In fact, I wear a large number of scars from fixing Firefox bugs in JavaScript (or C++) code. These days, I prefer strong, static typing. I sleep more soundly :)
But, as usual, we're not in a one-size-fits-all industry. There are developments for which static typing gets in the way of zero-to-deployment.
As usual, tradeoffs everywhere!
Note: Strongly + statically typed languages also typically offer strong support for other forms of static analysis (e.g. model checking, abstract interpretation, etc.) that are considered critical in some industries (e.g. aeronautics – or writing Windows device drivers). But we're getting into something of a niche.
What about Dart doesn't feel sufficiently typed for you?
Apparently, these days, Dart claims to be sound... except they seem to have redefined the meaning of type soundness along the way: https://dart.dev/guides/language/type-system#runtime-checks
I've certainly seen that happen in Ruby, but I've also seen that happen in every other language - most of them statically typed - I've worked with, as very few languages have type systems expressive enough to prevent it (and it tends to massively destroy productivity to try check everything strictly enough anyway).
IIRC someone looked at Github issues a while ago to understand the impact of dynamic typing on defect rate. What he found was that around ~1% of bugs in JS/Python/Ruby enterprise systems were type-related.
It might be just anecdotal, but I noticed people often say that writing in Ruby makes them feel happy.
Static typing (or functional programming, or TDD) doesn't seem to make people happy. It makes them feel superior.
Whether it truly makes them superior, or just taps into their insecurities is an open question.
These days more inferred typing in many of the static languages has lessened the gap, so the reason to hate on them has been reduced.
I still would love better static analysis tools, including for Ruby, but at the same time the way I write code has changed drastically in ways that makes that harder (e.g. leveraging the dynamic features of Ruby more)
Ruby specifically is a separate question IMO because it has a lot of convenience features that are kinda orthogonal to the whole typing debate. Crystal is a good example of how you can retain those in a statically typed language.
https://en.wikipedia.org/wiki/Crystal_(programming_language)
But keep in mind this includes run-time dynamic meta-programming. E.g. one of my projects bootstrapped a large proportion of the web frontend by having the backend introspect the database schema and dynamically generate models, routes, access control and metadata endpoints that the frontend then used to instantiate the UI.
The typing in that case was controlled by the database migrations.
I've done "generate a whole CRUD backend from a single source of truth for what my domain looks like", although I've done it by defining the model classes, generating the database schema from them, and deriving everything else from them. All the building blocks are there but I haven't found a framework/library that puts it all together (that's one of those projects I keep meaning to get around to). I've heard there are some database access libraries that let you use a macro to derive model classes from the schema, but I haven't used them yet.
If you see static typing as essential, then I can see the appeal. But my starting point is that it's not essential to me (and we're increasingly getting part of the value via optional and gradual typing via things like Sorbet which also let us keep the clutter out of sight, reducing the gap even further), and so losing even some of what brought me to Ruby is too much of a sacrifice.
Unfortunately, most people got a taste of static typing with Java and the initial language and library design were done in such a way that types were well, if not entirely useless, then certainly under-used. But if you look at many libraries for languages such as Rust, Scala, OCaml, Haskell, F#, ... (I haven't looked at Java in a while, but I don't hold high hopes on this specific front) you'll find many examples in which the API and the type system cooperate to guarantee that high-level protocols are enforced.
That being said, I absolutely agree that strong static typing does not mean that you don't need tests. As everything, if you want to be safe, you need a defense in depth, with good API design and many test layers.
The solution is to become a software atheist and just break things down when they no longer fit in your head.
Interestingly, this seems to be what microservices originally were about until they got twisted by hype.
It is also what OOP was about, until it got twisted by another religion.
Sure, test for sane failure modes in line with the documented contract, and that may include the occasional test that is de facto a type test, but often testing for types, especially in languages with poor type systems, but also in Ruby where we have alternatives, ends up with tests for classes which is frequently the wrong thing.
E.g. in Ruby never, ever check for somearg.kind_of?(IO) if all you ever do is somearg.read - if you absolutely must typecheck somearg, the Ruby way is to check for presence of "#read", e.g. somearg.respond_to?(:read), or try and fail responsibly (and often just allowing the NoMethodError to bubble up is the right way to fail).
Also think it's just fine for people to add and ship Sorbet type declarations for gems etc. to then signal those contracts so people can verify them if they choose. There's no reason not to offer that when it's reasonable to do so.
In Python or JavaScript/TypeScript, though, my experience is that failing to validate types at the borders (i.e. every single function/method/constructor/generator/... exposed in your API) is pretty much guaranteed to end up, months later, with developers attempting to sherlock out surprising breakages in production from logs that make no sense and traces that do not show anything remotely close to the actual culprit.
I have the scars to show it :) Of course, YMMV.
Hence don't check for an IO object when what you care about is the presence of a "#read" method.
Or don't check if something is an Array if what matters is that it supports "#map". Instead, either somearg.respond_to?(:map), or if it needs more than map, somearg.respond_to?(Enumerable) (this seems broad, but supporting Enumerable only requires implementing "#each" and including the module, so a caller "worst case" can reopen a class or wrap their object), or call one of Array(somearg) (tries "#to_ary" then "#to_a" then falls back to returning [somearg]) or Array.try_convert(somearg) (tries "#to_ary" then falls back on nil).
Consistently picking the most generic applicable options when faced with choices like that still enforces the contract on the boundary, but also tends to lead to code with much less ceremony. E.g. far fewer [something]Adapter classes, or glue code to convert data before calling methods that are being overly prescriptive.
But for most statically typed languages, when people talk about static typing, they still talk about a class and a type as interchangeable (there are exceptions, and it's getting better, and with increased type inference coupled with increased support for type annotations and analysis of dynamic languages, I expect there to be an increasing convergence, though).
Note that I'm writing "types" and not "classes". Interfaces do just as well, if not better!
For what it's worth, in the static realm, OCaml introduce features that let code check for presence of a method (instead of implementing an interface) ~25 years ago. Unfortunately, this proved rather unwieldy and, to the best of my knowledge, nobody uses that feature.
When there is a meaningful name to give, in Ruby you might define a module, and include it and use that as an interface:
module HelloWorld
def hello
raise "Not implemented"
end
end
class Foo
include HelloWorld
end
Foo.new.hello # Raises an exception
Foo.new.kind_of?(HelloWorld) # Returns true
(And anytime you want something that behaves somewhat like an ordered collection, you typically just want object.kind_of?(Enumerable)).There are plenty of contract-checking frameworks for Ruby [1] but they see little use, because most of our contracts tend to be extremely simple. Along the line of "implements method X" or "includes module Y".
Sorbet w/inference tools and optionally storing the types in a separate files (it supports inline too) might slowly change that (and effectively let you use modules as "proper" interfaces), since the biggest opposition to types in Ruby tends to be visual noise and forcing refactoring, and if it can be turned on/off in your IDE and regenerated, a lot of the objections fall away. It's not that we (well many of us) don't want the extra type checks, but that we don't want to pay the cost of making the code less readable.
[1] Here's one: http://egonschiele.github.io/contracts.ruby/ and of course there's Sorbet: https://sorbet.org/blog/2020/07/30/ruby-3-rbs-sorbet
But to counter myself, the perceived lack of developer speed and flexiblity of a strongly typed language (with Rust, I always get the feeling that I'm fighting with the compiler!) is also solved in the long run with practice and tooling.
If you have a preexisting codebase I believe the way you can convert it is to add the types that you know on commits and eventually you will have enough types that adding the missing ones should be easy. For the missing ones Any is a good choice.
https://pyre-check.org and https://github.com/python/mypy are popular.
One hurdle I've stumbled over recently is the question "what is a type?", the answer can be surprising. Unions, for example, are types but not `Type`s. A function that takes an argument of type `Type` will not accept a Union. So if you want to write a function that effectively "casts" a parameter to a specified type, you can't. The best you can do is have an overload that accepts `Type` and does an actual cast, and then another that just turns it into `Any`. This is, in fact, how the standard library types its `cast` function [1]. The argument I've seen for the current behavior is that `Type` describes anything that can be passed to isinstance, but that's not a satisfying answer. Even then, `Union` can be passed to isinstance and still does not work with `Type`. Talk currently is to introduce a new kind of type called `TypeForm` or something to address this, which is certainly an improvement over nothing, but still feels like technical debt.
[1]: https://github.com/python/typeshed/blob/main/stdlib/typing.p...
YMMV
https://github.com/Shopify/tapioca
Tapioca is used to generate type signatures on gems and code that creates functions at runtime. Sorbet ships with some of that behavior, but they updated their docs to recommend tapioca over sorbet where the behavior overlaps.
(Ruby is strongly typed in the "doesn't automatically coerce types" and "maintains a fixed type for an object" sense; using the term "strong typing" is inherently ambiguous because it has so many partially conflicting meanings)
Monkeypatching is evil.
Rubocop custom cops to reign-in what's allowed in CI/CD.
But anytime someone introduced a new gem (namely act_as_paranoid) or writes a new concern that modifies defaults, then you're back to need to do live code.
Ruby requires you to be aware of much more "state" than any other language that I have coded in, because all assumptions about methods cannot be confirmed until the code is run. For example, a gem might modify the default_scope of a portion of active record models. You have to memorize all these tweaks and even then you don't know if the code you've written will do what you want until it is actually been run.
https://survey.stackoverflow.co/2022/#technology-most-loved-...
At Stripe, there are millions of lines of Ruby. Lots of it would be better off in a different language—if it had been written in another language initially. Ruby is easy to hire for, there are tens of thousands of pages of documentation for the code written in it, and there's a ton of operational knowledge about how to use it well. Switching is possible, but the cost to replace it is half a decade or more. During that time, you're not building new features, you're worrying about compatibility and making sure there's no downtime. Instead of making incremental improvements, your infra folks are worrying about migration paths.
At the end of the day, the major arguments against Ruby is lack of native type checking (solved with tooling) and performance. For Shopify, you can either undertake a major engineering effort to rewrite your stack (years of effort, incidents, no real feature development, morale killing) or kick a few million dollars at making the thing your engineers like faster. The cost of replacing Ruby is tens of millions of dollars: not just engineers putting pen to paper, but lost business from putting the company on hold, breakages, and throwing out immense amounts of operational knowledge and experience.
Said another way, telling a thousand+ people to drop what they're doing, learn a new language, and redo all their work is an expensive way to end up with a worse version of what you already have. It's not a sunk cost fallacy if you literally can't afford to change.
Ruby is easy to hire for
I've always had the opposite experience!Ruby is so powerful but simplicity is the best if other people are going to read and maintain your code. Go is great for this.
Ruby is so powerful but simplicity is the best if
other people are going to read and maintain your code.
Amen. Keep it simple for a large shared Ruby application, such as a Rails app.More advanced Ruby (writing your own operators/iterators/whatever, metaprogramming, extending the language itself, whatever) is best reserved for Ruby frameworks, not applications.
When I've been on the hiring side of the equation, it's been very tough to find candidates.
I would imagine if you're going to switch millions of lines from one language to another - you wouldn't do it by hand - you'd write something transpile it?
Has any company actually done something like this before - especially somewhat recently?
I can't imagine anyone doing it by hand...
That's not to even mention the massive amounts of new bugs that would be put into the code that have been ironed out over years of debugging and testing in the Ruby base. There's a reason so many legacy operators still run COBOL despite even C being a better candidate, it was just created twenty years too late and the costs of moving it over is well outside of the savings of not doing that.
"If it ain't broke, don't fix it" is very much the key here.
This kind of performance comes from lots of resources invested in tuning. Lots of resources are invested, generally, when there are lots of entities relying on a thing; that's a very good reason people invest in improving a thing, right?
So I'm not really sure what you mean -- you say "sunk cost", I say "Well, those who are using a thing are those who invest in improving it, how is it ever any different? And that there are people with current investments in ruby who will continue to invest to improve it is what will make ruby continue to thrive -- and how is it ever any different with any technology?"
What would make it "sunk cost", i suppose, is if you think ruby is a poor technology and everyone using it should switch to something else. I guess it is popular to hate on ruby right now, but that seems to be an orthogonal debate. Ironically, one of the most popular reasons to hate on ruby is "performance" (rightly or wrongly), so it seems especialy weird to me to show up with an argument like "Sure, they're drastically improving ruby performance, but is ruby the right choice of thing to improve it's performance? After all, ruby has such bad performance!"
To be fair, you didn't specifically say "because ruby has such bad performance", you didn't say anything about why ruby might be the wrong thing to invest in at all -- which makes it all the more just weird FUD, intentionally or not.
You are suggesting that just in general we should prefer switching to new languages over investing in the existing ones? Since you didn't supply any specific arguments, it makes it seem as if you suggest this as a general principle, regardless of details? I think many of our experiences is that this leads to always using immature technology; to reach the stability and performance of (say) the JVM or V8 requires... investing in the thing, not constantly chasing a new immature thing hoping it will be different this time.
It's going as strong as ever and, if anything, it's stabilised into a robust, expressive and powerful language and in many cases that's a pretty acceptable trade-off. The fact it has long since matured into 'boring' technology (as in 'choose boring technology') is nothing but a good thing.
It's great that so much work is going into performance, though. I've been excited to try this new JIT out in prod, and I'm excited to see how Ractor, for example, evolves.
From what I remember, HipHop was distributed in a different toolchain than the vanilla PHP interpreter. Ruby also have other interpreters available by the way: https://github.com/codicoscepticos/ruby-implementations
We didn't want to independently reimplement Ruby because we knew that this would lead to a situation where we wouldn't be 100% compatible, which would stop people from using YJIT. If you think about PyPy for example, they have great performance numbers, but relatively few people are using it.
I get your sunk cost objection, but it's a bit depressing to hear people worry about someone doing genuinely useful work. Nobody wants to take responsibility or get their hands dirty. There's still plenty of low-hanging fruit in both language implementations and databases. The world today is just mountains upon mountains of bullshit.
It's also important to me to give back to open source. It's done so much for us.
irb > def method
irb > 'yes'
irb > def method
irb > 'it is not'
irb > end
irb > end
irb > method
=> :method
irb > method
=> "it is not"It's internal expertise in that language, understanding of its strengths and weaknesses and how they fit into your business needs, custom tooling around dev, debugging & deployment, practice evaluating candidates for expertise in it, particular strengths of that language not guaranteed to be in a replacement, just all of the "unknown unknowns" that you now know through bitter experience.
Surely there will still be a point where it's worth throwing all that out and starting fresh, but some of those are very hard to quantify or even see, when you have them, so the conservative choice is to stay put in the system that works.
There is no magic bullet. You can't do an overnight transition. If you decide to make a language shift (or equivalently large architecture shift), you need an interop. Old and new have to coexist. The remaining 30% (where interop fails) is where engineering happens and is often where you had hidden technical debt anyway.
For side projects, I use Rails 7 to get things going and replace components selectively with native extensions and separate processes.
It's all about productivity, especially when small, because performance is rarely an issue.
I can remember about 8 years ago someone telling me how they were worried Ruby (and Rails) was dying. At this point the community could dry up and I think I'd still stick with it.
Python without its crazy amount of extensions in c-api won't survive.
I do hope it takes off.
For quite a while it used to have some terrible performance if you ever reached out to C libraries in a hot loop. It's still a bit iffy on the C / FFI front, though the performance issues are much improved
It's certainly good enough for production use, with careful evaluation.
That doesn't say anything at all sadly.
Ruby people seem to be sensitive when someone asks them about performance, my last question about that was downvoted heavily here.
I don't know any Ruby, I know how much Go can handle with the stdlib, a single non-parameterized route, returning "hello world". On my machine xeon e3-1275v5 with 8 threads 1000 concurrent wrk about 220k/ rps. Errors start appearing (aka non-200 results) between 5000 and 6000 concurrent. Don't forget to ulimit -n 10000 or more before you start wrking.
Alternatively can you provide a simple single route hello world in Ruby so I can bench it myself with this Ruby 3.2 YJIT? Maybe also how to build and run?
Yes, I'm being serious. I'm also guessing that Ruby with Rails has a large overhead compared to Go and just the stdlib. So that comparison wouldn't be fair, but I guess it is what it is, since we're talking semi real world scenarios (yeah don't get your ocd panties in a bunch) and Rails AFAIK is the standard for Ruby web development while the stdlib in Go is also used quite often.
A single non-parameterized route returning "hello world" is a real-world scenario? You have an interesting business domain!
If you tell me that Ruby and Rails scaled for Github and Shopify, be careful using that as an argument for choosing Rails for your company. Rails definitely didn't scale for Github, and doesn't appear like it scaled for Shopify, until a manager greenlit a year long JIT project unrelated to Shopify. If your company has as many resources, free cash, klout to hire core Ruby maintainers, and enough downtime you can throw a few engineers at a non-product problem for a few years, then sure, go ahead and use that reasoning.
Sure, if you're a startup and you can't afford it, you probably shouldn't be spending time speeding up your language. But, you're also unlikely to experience performance issues under your lighter load that can't be attributed to your application.
I run many small servers (2 processes each), but IIRC, Shopify runs beefy servers. So may be different for them.
Not sure were you've seen this, but that's absolutely not normal. The YJIT memory overhead is certainly sizeable, but is generally at least an order of magnitude smaller than that.
My production workload shows similar results. The YJIT app's memory sits at about 2.4x what my non-YJIT app utilizes. Sidekiq sits at 3x.
Well, maybe Elixir and Erlang scales out, since architecture is baked in.
Depending on how performant a language is, it can put off (sometimes forever) your need to scale out, which can simplify things. But both Github and Shopify are beyond the level of traffic that would allow you to get away with only scaling up rather than out.