> 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)
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.
Hey lemming! You're right, which is why it should be used sparingly. Since clojure.core is compiled (on the JVM) with direct linking, reacting to var changes isn't an intended concern, since they're not going to work properly throughout any clojure.core code using that var. This makes it a good candidate ns for inlining things. But users shouldn't just be doing this for their normal application vars without giving it due consideration.
I spoke with a couple Clang and LLVM devs about MLIR when I was doing the original design for jank's IR. The general consensus was that MLIR added a great deal of complexity on top of designing/implementing an IR and nobody was confident it was actually worth the effort. Since I knew exactly what I wanted, I just built that.
The 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.
Probably 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?
Very 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.
You 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.
13 comments
[ 0.95 ms ] story [ 41.5 ms ] threadThe 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.
> 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)
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.
Right, as you said, you'd have to recompile dependents.
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.
https://carp-lang.github.io/carp-docs/LanguageGuide.html
Has anyone been playing with it on HN ?
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.
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.