5 ms·
Jank now has its own custom IR
- christophilus 5mo ago> we're using it to optimize jank to compete with the JVM The JVM gets a lot of hate, but that is a very high bar. The JVM is a serious piece of kit. I hope Jank succeeds. I'd love to use it in real projects.
- pjmlp 5mo agoAdditionally, there are many JVMs to chose from, many always make the mistake to equate JVM with OpenJDK, which is like talking about C and only considering GCC or something. Other JVMs have plenty of goodies, some of them have AOT for about 20 years now, others real time GC, other ones JIT caches before Project Leyden was even an idea, others actual value types as experiment (ObjectLayout on Azul), pauseless GC, cloud based JIT compilers, bare metal deployments, ART also has its goodies somehow despite everything, there is a whole world that is lost when people focus too much on JVM == OpenJDK.
- let_rec 5mo agoOn the other hand, the JVM spec may prohibit some optimizations you are after. It's very dynamic after all!
- pjmlp 5mo agoNot really, that is the usual argument why CPython is slow. If anything runtimes like the various JVM implementations, alongside the CLR and JS engines as well, are the bleeding edge of dynamic compiler optimizations with dynamic runtimes. That is something that gets lost when talking about Java, yes the programming language looks like C++, however the JVM itself is heavily inspired by Smalltalk and Objective-C dynamic semantics. Coming back to the spec, you will notice that it doesn't mention how threads are implemented, what kind of AOT/JIT are available, or what GC algorithms to implement, leaving enough room space for implementations. One area where you are actually right, that I just remembered while typing this, are the way reflection or unsafe code hinders some optimizations, hence the ongoing steps that enabling JNI or FFM has to be explicit at startup, dynamic agents also have to be expliclity enabled, and the upcoming final means final (no more changing final fields via reflection).
- truth_seeker 5mo agowhat really matters is : how far can i get in X programming language by writing just idiomatic code? how much of SDK and community libs, frameworks help me run my program at bare metal speed ? What sort of change i have to do exisitng libs, frameworks and my legacy code for CPU, IO and memory efficiency as a migrate to new version ?
- pjmlp 5mo agoThat is only part of the picture, the other part that seems quite forgotten nowadays is: - how much people actually care about algorithms and data structures - do they actually know what options their tools have available - have they ever spend at least an hour reading the man pages, info page or HTML documentations - have they ever used a profiler, a graphical debugger, an advanced IDE
- drob518 5mo agoIndeed. Most of the hate is due to slow start-up time, but once it gets warmed up, the modern JVM has state of the art dynamic compilation and GC. Thousands of man years have been spent getting it to that point.
- arikrahman 5mo agoWith Project Leyden being implemented and JEP 516 coming very soon, those worries will be a thing of the past. Now you can get incredible AOT performance without having to depend on Babashka or GraalVM workarounds.
- drob518 5mo agoYep, and I’m totally looking forward to it.
- mccoyb 5mo agoHoping to understand this better: > Clojure's dynamism is granted by a great deal of both polymorphism and indirection, but this means LLVM has very few optimization opportunities when it's dealing with the LLVM IR from jank. In my mind, what is happening here is you lower Clojure code into LLVM, with a bunch of runtime calls (e.g. your `jank::runtime::dynamic_call`) (e.g. LLVM invoking the runtime over a C ABI). If that's true, are there any optimizations that LLVM helps out with? Perhaps like DCE? I can't tell immediately, curious about the answer (question is obviously about the pre-IR state of things)
- codebje 5mo agoThe article talks about inlining a two-arity call to clojure.core/max to instead be an explicit call to cpp/jank.runtime.max, eliminating the unnecessary argument count matching and recursion portions of the Clojure function. It also mentions that in Clang the runtime max function will itself be inlined, so that's something LLVM ("the LLVM project", anyway) is still doing - and beyond that, as written this IR is likely to leave behind plenty of opportunities for LLVM to do the things it's good at: DCE, load/store optimisation, constant propagation, etc. And register allocation. The jank::runtime::max call is itself complex: it's got to type check its arguments and work out what to actually do based on the two types; if parts of these tests are done before the inlined call to max there's a fair chance that LLVM will be able to eliminate their repetition and slim it all down a long way. In the fibonnaci example the fact that a previous test will have likely identified whether the argument is an int or something else should hopefully carry over for ::lte, ::sub, and ::add and simplify those down to just the single operator call - but sadly I suspect it won't at least for the addition, because the recursive call will lose the information that the return value when called with a tagged integer is always a tagged integer. A future optimisation might be to specialise for unboxed types: far more potential speed improvement over pointer tagging, and IMO quite amenable to analysis with the Jank IR (:metadata tag functions as specialised for <type> with the new entry point, if a function only calls specalised functions (and itself) it too can be specialised, and a heuristic to determine if specialisation gains enough to sacrifice space for it).
- Jeaye 5mo ago
- lemming 5mo agoGreat article, as always. There is one thing that I think is important to bear in mind when discussing inlining, especially in the context of Clojure. This is that once a function has been inlined, you can no longer update the definition of that function in the REPL and have that update the behaviour of functions which use it, unless you recompile those as well. This is not a criticism of course, it’s just part of the natural tension between dynamism and performance.
- sieabahlpark 5mo ago[dead]
- thfuran 5mo agoDoes that not happen automatically? I know there are contexts in which jvm will deoptimize inlining and recompile, like in response to class loading that causes a call site that was previously provably monomorphic to no longer be.
- lemming 5mo agoNo, it doesn't. In JVM Clojure's case, the vars are usually compiled to the moral equivalent of a global variable holding a pointer to a function. This allows you to update the function if the developer redefines it in the REPL, but it comes at a performance cost (the JVM can't inline it or otherwise optimise it). Clojure also allows you to compile with "direct linking", e.g. for production deployments, where you know you're unlikely to be wanting to dynamically update the code. In those cases defns are compiled down to static methods which call each other - much faster since the JVM can perform its magic with them, but you can't update them at the REPL. I'm unsure exactly how jank works WRT this tradeoff, but the article makes it sound like it's closer to the direct linking version, but with the inlining etc being done by jank rather than the JVM. I don't know if this is only for AOT or also in JIT cases.
- arvyy 5mo ago> the vars are usually compiled to the moral equivalent of a global variable holding a pointer to a function. This allows you to update the function if the developer redefines it in the REPL, but it comes at a performance cost (the JVM can't inline it or otherwise optimise it) might be out of my depth but I find it surprising; I thought compilation through invokedynamic should be able to handle redefinition while still allowing inlining and other jit optimizations
- CalChris 5mo agoThe natural question is why doesn't Jank use MLIR?
- pjmlp 5mo agoNo language using MLIR uses it directly out of the box, in that sense the right question is why did Jank not create their MLIR dialect.
- debugnik 5mo agoMLIR dialects have to be lowered into the basic LLVM one eventually, don't they? Does MLIR add anything over a custom IR for host languages that aren't deficient at manipulating data structures?
- eigenspace 5mo ago'MLIR dialects' is just a term for teaching MLIR how to manipulate and understand your own custom IR. MLIR is just very good at producing good vectorized code in the presence of stuff like nested loops compared to LLVM or even some of the most carefully crafted custom compilers. It's not about whether your custom compiler is 'deficient' at handling data structures, MLIR is just genuinely very good at some of this stuff compared to basically anyone else. For most projects it's just more trouble than it's worth though, because maintaining and using an MLIR dialect definition is hard.
- debugnik 5mo agoBut AFAIK those aren't features of MLIR, but of lowering to existing MLIR dialects and running their passes. My genuine question is whether these passes provide any benefit before lowering, because otherwise a custom dialect doesn't add anything over lowering from a custom IR for anyone not using C++; and the only example I've seen is forced inlining.
- erichocean 5mo agoI suspect the author created his own IR after being offered that suggestion earlier, he's definitely aware it exists.
- pjmlp 5mo agoThe natural evolution of compiler toolchains that live long enough on top of LLVM, eventually every one matures into having their own IR. Even clang is now in the process of doing the same. > We're going to use Clojure JVM to get our baseline benchmark numbers and then we'll aim to beat those numbers with jank. > Note that all numbers in this post are measured on my five year old x86_64 desktop with an AMD Ryzen Threadripper 2950X on NixOS with OpenJDK 21. When I say "JVM" in this post, I mean OpenJDK 21. In 2026, a better baseline would be the Java 26 implementations of OpenJDK, OpenJ9, and GraalVM, with JIT cache across several execution runs. > In the native world, we don't currently have JIT optimization. It could exist, but LLVM doesn't have any implementation for it and neither does any major C or C++ compiler Yes they kind of have, that is partially what PGO is used for, to get the program behaviour during training runs, and feed it back into the compilation toolchain. Also while it isn't native code per se, when targeting bytecode environments like IBM i, WebAssembly, CLR, among others, with C or C++, there is certainly the possibility of having a JIT in the picture. > Finally, just because jank is written in C++ doesn't mean that we can escape Clojure's semantics. Clojure is dynamically typed, garbage collected, and polymorphic as all get out. Which is why, benchmarks should also take into account compilers for Common Lisp and Scheme compilers. Anyway, great piece of work, and it was a very interesting post to read, best wishes to the author finding some support.
- stavros 5mo agoIsn't the main benefit of LLVM that you get tons of backends for free? What does having your own IR give you that's worth this tradeoff?
- eigenspace 5mo agoThese compilers aren't replacing LLVM, they are adding a compilation step with its own IR where they do certain optimizations and translations *before* handing things off to LLVM. Basically, the idea is to do as much 'high level' optimization and transformation stuff as you can in your own IR, and then let LLVM handle the low-level stuff and the targeting of specific hardware vendors.
- mocompute 5mo agoProbably a stupid question, but is LLVM better at optimising its IR than C compilers are at optimising C? Asked another way, why not use C as an IR, if it's compatible with your language semantics?
- eigenspace 5mo agoLLVM is essentially what you get when you say "I want to use C as an IR", and then try and do it for a bit and say "hmm, okay I'd like to put some restrictions on this IR... and maybe some customization hooks... and maybe this feature..."
- adgjlsfhk1 5mo agoC is a really bad IR for a lot of reasons. it has incrediby opinionated semantics (e.g. the huge amount of UB and platform specific behavior). LLVM is a lot more verbose, but allows you to actually pick the semantics you want.
- srean 5mo agoThis reminded me of Carp https://carp-lang.github.io/carp-docs/LanguageGuide.html https://carp-lang.github.io/carp-docs/LanguageGuide.html Has anyone been playing with it on HN ?
- iLemming 5mo agoVery cool. My immediate thought was - could this open possibilities to compete with Rust on wasm targets? Upon reviewing it more I realized - probably not. Jank (or any other Clojure dialect) wont' really be in position to beat Rust there, and reasons are structural, not just engineering effort - we need GC and, well, that's the biggest elephant. But to be completely honest, the question: "do you need wasm at all...?", should be always followed by "why?". For like 95% of cases, Clojurescript saves you weeks/months of work. Easier to build, easier to maintain. That's subjective, of course. Most Rustaceans don't even want to try Clojure. Most Clojurists find Rust to be needlessly complex.
- IndianAISupport 5mo ago[dead]
- deterministic 5mo agoYou almost always want your own IR that you can use for high level optimisations and then transform it into the LLVM IR. LLVM at the IR level has no understanding of the semantics of your language and therefore can't do the kind of optimisations that will really make a big speed difference. I ended up having two custom IR's for a very high performance compiler I maintain at work. It made a big difference.