Hacker News new | ask | show | jobs
by stavros 38 days ago
That makes sense, thanks. Is this IR at a level where the optimisations can't just be added to LLVM then?
4 comments

I don't know much about Jank's implementation, but I can speak to how it's done in Julia (dynamic, high performance language with lispy semantics but matlaby syntax, JIT compiled to LLVM).

I think the big thing is just that LLVM can't really be made to closely model everyone's different weird langauge semantics. In practice, the less C-like your language is, the more hoops you will likely need to jump through in order to prepare your code to be handed off to LLVM if you want to get a good result out of it, otherwise it just wont understand your code well enough to make good optimizations, or may not have the proper optimizations implemented.

Trying to modify LLVM to fit your purposes is a bit of an uphill battle too. You either have to try and convince all the stakeholders that each one of your proposed modifications are worth it (when they're typically just not needed by C-like languages), or you need to maintain a fork which is a nightmare.

Like, just to take one example, Julia has a world-age system I describe here: https://news.ycombinator.com/item?id=48151251#48177215 which most other LLVM users would have no use for, and would just add complexity and overhead for them so I don't think any julia people ever even thought about trying to upstream that.

Julia is a somewhat extreme example. It actually has like 2.5 different IRs internally because it just does a lot of compiler transforms before handing things off to LLVM. We've generally just been on a trajectory of moving more and more stuff over to the Julia side because it gives us maximal control.

Another example: For years rust was limited on performance optimizations in LLVM. Specifically, it was difficult to get LLVM to properly optimize for Rust's generated code, namely where one can make strong aliasing (and non-aliasing) statements using `noalias`. This is a (pre-existing iirc) LLVM attribute.

Despite being a pre-existing feature motivated by C-like languages, typical C/C++ code does not leverage this attribute that much. So there were a surprising number of bugs in the handling of the attribute, and it took a number of years (I didn't follow things closely, but >= 3 for sure, maybe as much as 6?) before they got ironed out enough where it could be enabled.

As another said, jank is not replacing LLVM or LLVM IR. We still use LLVM IR! There is a diagram here which shows the pipeline: https://book.jank-lang.org/dev/ir.html

The main thing is that we just use our own IR first, to perform optimizations with contextual data which is gone by the time we get to LLVM IR. That's also why these optimizations are not practical to write in LLVM, since by the time we get to LLVM IR, we're too far separated from jank's AST with the high level semantics of Clojure.

So we just add an intermediate step. Once we have jank's AST, turn it into our own IR, do some optimizations on it for things that LLVM won't be able to see, and then hand it off to LLVM to do the rest.

Ahh OK, makes perfect sense, and interesting that that IR compiles to C++. Thanks for the info!
one example of this is type inference. llvm is a statically typed ir, so if you're compiling to it from a language with an expressive type system (dynamically typed or statically typed with generics), you need to do your type inference pre llvm.
In my uninformed opinion it's like the SIMD discussion from yesterday. Without their fancy SIMD library, the optimization [`sqrt(x) * sqrt(x)` === x] gets lost in a sea of C++ template incantations when using that SIMD library.

Similarly, perhaps, if there's some fancy observation of an invariant that can be made about `*.map(...)` that gets "lost in the sauce" once it's been lowered to the typical push/pop/loop mechanisms, then those higher level optimizations are better done in a language specific IR, not the "default" IR.

It's actually IR's all the way down if you think about it...