wtf is Clojure inlining anyway
If you spend your time digging around clojure.core
like I find myself from time to time, you might come across a interesting metadata keyword called :inline
. I have never seen :inline
in any of the non-core libraries in my career, so I've been digging around to figure out what the intent behind it is.
The term inline seems reminiscent of inline functions from C and C++. In short, inline
is a compiler directive to suggest that the compiler place the assembly/object code of the inline function in the calling code. The immediate benefit being all the effort for stack frames aren't necessary, very important in the days when hackers only had thousands of CPU cycles to work with. Not so important today, or is it?
What it looks like
Clojure's inline functions serve a similar purpose, telling the Clojure compiler to use this bit of code directly. Let's take a look at one:
The :inline
keyword has a value of a function with fixed arity, returning a syntax quote much like a macro, or "function template", yet it still has a normal function body as well. When calling the function directly the Clojure compiler will opt to use the inline version, and avoid the the lookup in the symbol table. However, if using the function as a callback, the compiler will opt to do the lookup. Let's look at a simple Clojure example1:
(ns inline-fun.core
(:gen-class))
(defn -main
"A very complicated function"
[& args]
(neg? -1)
(filter neg? [1 -1 2]))
Looking at the invokeStatic
method of the decompiled Java class, we can see the Clojure compiler did not opt to look up the var for neg?
like it did for filter
and filter's predicate2.
Since it has the same function body as it's inline,neg?
is a bad example to use, but we can see why the inline function template uses the Java interop syntax. A call to a Clojure function would cause an error because the complier skips fn parsing all together if the inline exists, applying it to the next immediate form in the s-expression. Intuitively, inline functions feel like they should be performant though I'm bit skeptical about the performance pay off here.
Performance
I decided to do some benchmarking to see if it's worth the effort, and I switched to inc
for this test:
There's a few caveats with this test. First, because I wanted a somewhat fair test, I wrapped the inline function in a lambda since the fn
object produced by the compiler inlines inc
for us. Second, the sample size could be better. Lastly, because the inline function template uses the faster unchecked math functions, it may skew the results toward the inline function, but it's good insight into how an inline function should (or shouldn't) be written (with the hacky fast bits). All that said, I still think we got some results to inform our decision making here.
Based on the bench
form provided by cirterium, we can see some marginal improvement from inlining with sufficiently large input on the order of hundreds of microseconds. Not exactly a huge jump, but might be worth the effort when dealing with incredibly large input (though you shouldn't be using eager collections with such input anyways).
When to use it
After all the benchmarking, you might be itching to rewrite the slow library drawing your ire, but think twice before you do. Remember the compiler skips parsing all together if the inline exists, so macroexpand-1
won't save your ass here. If you're writing a Clojure(script) library3, Clojurescript does not support inlining, and the compiler will ignore the metadata keyword. If at all, my suggestion would be to use it in time critical applications with large datasets, like an ad exchange bidder, though it may be more advantagous to look at GraalVM's native-image capabilities instead.
Conclusion
There you have it, Inline metadata in Clojure def
tells the Clojure compiler to directly substitute this call for the function template provided and is best suited for repeated symbol table look ups like large reducers or time critical applications.
Update
Alex Miller read this post and told me about the true benefits of Clojure Inlining, so I apologize for making you read the poor benchmarking above. I'll take a run at a better test in the future.
1. This example is by and large a recreation of the example Alex Yukashev uses, and serves as the technical inspiration for this post. You can find his post here.
2. Technically these symbols would have been created if they didn't exist, see interning for more details.
3. That is, a library supporting both Clojure and Clojurescript; similarly, your library has files ending with .cljc