What is the difference at the JVM bytecode level between these two calls? Is there any performance difference, especially in hot-paths?
(.glClearColor gl (float 0) (float 0) (float 0) (float 0))
vs
(.glClearColor gl 0 0 0 0)
Maybe https://github.com/clojure-goes-fast/clj-java-decompiler Can help you answer this.
And you can do it without any additional tools - just compile the code and view it with javap.
As for the second question - any performance difference is very unlikely.
Thanks for the reply, I'm going to use those tools 👍
I used clj-java-decompiler and I got this results;
Also got help from an LLM that summarizes like this;
With (float 24) — primitive path:
ldc2_w 24 // push long constant (8 bytes, on stack)
invokestatic RT.floatCast:(J)F // long→float, primitive-to-primitive
That's it. No allocation, no indirection. RT.floatCast(long) is just a cast.
Without — boxed path:
// static init (once): box each literal into a Long object
Long.valueOf(24) → stored in const__0 (Object field)
// every invocation:
getstatic const__0:Object // load boxed Long from static field
checkcast Number // runtime type check
invokestatic RT.floatCast:(Object)F // unbox Number → doubleValue() → cast to float
(dd/disassemble
(defn clear []
(.glClearColor (Gdx/gl) (float 24) (float 25) (float 26) (float 27))))
// Decompiling class: api/canvas$clear
class api.canvas$clear
Minor version: 0
Major version: 52
Flags: PUBLIC, FINAL, SUPER
public void <init>();
Flags: PUBLIC
Code:
linenumber 2
0: aload_0
1: invokespecial clojure/lang/AFunction.<init>:()V
4: return
public static java.lang.Object invokeStatic();
Flags: PUBLIC, STATIC
Code:
linenumber 3
0: getstatic com/badlogic/gdx/Gdx.gl:Lcom/badlogic/gdx/graphics/GL20;
3: checkcast Lcom/badlogic/gdx/graphics/GL20;
6: ldc2_w 24
linenumber 3
9: invokestatic clojure/lang/RT.floatCast:(J)F
12: ldc2_w 25
linenumber 3
15: invokestatic clojure/lang/RT.floatCast:(J)F
18: ldc2_w 26
linenumber 3
21: invokestatic clojure/lang/RT.floatCast:(J)F
24: ldc2_w 27
linenumber 3
27: invokestatic clojure/lang/RT.floatCast:(J)F
linenumber 3
30: invokeinterface com/badlogic/gdx/graphics/GL20.glClearColor:(FFFF)V
35: aconst_null
36: areturn
public java.lang.Object invoke();
Flags: PUBLIC
Code:
linenumber 2
0: invokestatic api/canvas$clear.invokeStatic:()Ljava/lang/Object;
3: areturn
static {};
Flags: PUBLIC, STATIC
Code:
linenumber 2
0: return
=> nil
--
(dd/disassemble
(defn clear []
(.glClearColor (Gdx/gl) 24 25 26 27)))
// Decompiling class: api/canvas$clear
class api.canvas$clear
Minor version: 0
Major version: 52
Flags: PUBLIC, FINAL, SUPER
public static final java.lang.Object const__0;
Flags: PUBLIC, STATIC, FINAL
public static final java.lang.Object const__1;
Flags: PUBLIC, STATIC, FINAL
public static final java.lang.Object const__2;
Flags: PUBLIC, STATIC, FINAL
public static final java.lang.Object const__3;
Flags: PUBLIC, STATIC, FINAL
public void <init>();
Flags: PUBLIC
Code:
linenumber 2
0: aload_0
1: invokespecial clojure/lang/AFunction.<init>:()V
4: return
public static java.lang.Object invokeStatic();
Flags: PUBLIC, STATIC
Code:
linenumber 3
0: getstatic com/badlogic/gdx/Gdx.gl:Lcom/badlogic/gdx/graphics/GL20;
3: checkcast Lcom/badlogic/gdx/graphics/GL20;
6: getstatic api/canvas$clear.const__0:Ljava/lang/Object;
9: checkcast Ljava/lang/Number;
12: invokestatic clojure/lang/RT.floatCast:(Ljava/lang/Object;)F
15: getstatic api/canvas$clear.const__1:Ljava/lang/Object;
18: checkcast Ljava/lang/Number;
21: invokestatic clojure/lang/RT.floatCast:(Ljava/lang/Object;)F
24: getstatic api/canvas$clear.const__2:Ljava/lang/Object;
27: checkcast Ljava/lang/Number;
30: invokestatic clojure/lang/RT.floatCast:(Ljava/lang/Object;)F
33: getstatic api/canvas$clear.const__3:Ljava/lang/Object;
36: checkcast Ljava/lang/Number;
39: invokestatic clojure/lang/RT.floatCast:(Ljava/lang/Object;)F
linenumber 3
42: invokeinterface com/badlogic/gdx/graphics/GL20.glClearColor:(FFFF)V
47: aconst_null
48: areturn
public java.lang.Object invoke();
Flags: PUBLIC
Code:
linenumber 2
0: invokestatic api/canvas$clear.invokeStatic:()Ljava/lang/Object;
3: areturn
static {};
Flags: PUBLIC, STATIC
Code:
linenumber 2
0: ldc2_w 24
3: invokestatic java/lang/Long.valueOf:(J)Ljava/lang/Long;
6: putstatic api/canvas$clear.const__0:Ljava/lang/Object;
9: ldc2_w 25
12: invokestatic java/lang/Long.valueOf:(J)Ljava/lang/Long;
15: putstatic api/canvas$clear.const__1:Ljava/lang/Object;
18: ldc2_w 26
21: invokestatic java/lang/Long.valueOf:(J)Ljava/lang/Long;
24: putstatic api/canvas$clear.const__2:Ljava/lang/Object;
27: ldc2_w 27
30: invokestatic java/lang/Long.valueOf:(J)Ljava/lang/Long;
33: putstatic api/canvas$clear.const__3:Ljava/lang/Object;
36: return
=> nil
Hello, I recently asked a question on http://ask.clojure.org regarding defn not providing an implicit recursion point that has not been answered yet. Consider this example:
(defn fib [n]
(case n
0 0
1 1
(+ (fib (dec n))
(fib (- n 2)))))
Here, the recursive call of fib function doesn't call the function directly, but rather goes through dereferencing of the var #'fib , which is a waste of runtime performance.
Is there a reason for this? I believe Clojure could just implicitly create a named recursion point there, but it doesn't do that at the moment.
Also, another similar question: Why doesn't Clojure optimize tail recursion for named functions, like in this example?
((fn foo [x]
(when (pos? x)
(foo (dec x))))
1000000)
This code blows up on stack overflow, but I believe it should not be hard to internally replace this with recur if possible.Maybe Clojure does this, because you could have rebound the function before you call it. The compiler cannot know this. Clojure calls are Java calls. That is a design decision. I guess all this is way more complicated than we all think. And maybe it is not even perfect. For example what to do with Exceptions or with things that should happen in the moment when you leave the stack frame (e.g. destructors)?
> Tail Calls and Recursion: > I very much would have liked to copy > Scheme’s ‘proper tail recur- sion’ [Steele Jr and Sussman 1978] and > the elegant programming style it supports, but the JVM does not allow > for that stack management approach in any practical way. On the other > hand, I consider partial tail call optimization, e.g., of self-calls > but not mutually recursive calls, to be a semantic non-feature. In > addition, even with proper tail recursion, I thought it may be a point > of confusion among users when exactly a call is in tail position and > subject to the optimization.
section 3.2.3 from https://clojure.org/about/history pdf has great design decisions like this.
this thinking might be a hint at the first as well
I stil don't quite understand, why optimizing tail recursive calls should be a non-feature if it's just an optimization. In the end I care about whether my code doesn't fail and whether it's efficient, it does not break the code in any way. It should be very easy to detect, but sure, it's not as straightforward as the original issue.
Definining general recursive functions using defn (where the recursive call doesn't have to be in the tail position), is more common. There I really don't understand, why the inner function isn't named. The change is literally just replacing
(cons `fn fdecl)
with
(cons `fn (cons name fdecl))
(or possibly some meta attached to the name).unpredictable optimization is the issue i believe. If you think something should be a loop but it turns out at runtime to be a recursive call that can break unpredictably. Using recur ensures that you know precisely when you are consuming stack or not
How could this possibly break something? Using loops to eliminate tail recursion is just an optimization and it does not change what the program does. Instead of creating a new stack frame, it just replaces locals inm the current frame, but that does not break anything.
it does if you process 20,000 items. optimized it works, deoptimized it fails
So your worry is about the "it works on my machine" thing if the compiler decides to optimize it during development, but for some reason doesn't optimize the same code in production?
I’ll just point back to the authoritative source: the jvm doesn’t particularly love proper tail recursion, and the partial solutions didn’t appeal to the author. He made it explicit so it is never a surprise and gives this: > An advantage of recur is that the compiler reports an error if it > occurs in other than a tail position. This has been helpful in > practice—imperative programmers struggling to find how to do iteration > search for ‘loop’ and find it, then get a gentle introduction to its > recursive/ functional nature and help getting it right (the tail > check).
Well, I'm still confused, why having the compiler optimize code for you when it doesn't break anything is a "bad surprise"
But ok, if you think otherwise, there's still the main question that I've asked - Why doesn't defn implicitly name the inner function?
there’s something like that that comes up as well. last is always O(n), even on vectors where it could be O(1)
clojure.core/last
([coll])
Return the last item in coll, in linear timethe idea there is that if you are using a vector, it’s easy to swap to something that isn’t indexed and then performance can be catastrophic. Predictability is desirable, even if it could be faster.
Yeah, this is not ideal, but I think this is more similar to questions like "why doesn't clojure.set/union check for its argument types?" - checking the type at runtime would introduce some overhead that mostly is not necessary (because the arguments are expected to be sets). However, when optimizing tail recursive calls, there isn't really any overhead.
nor for last being o(1) on vectors
that’s not garbage input, it is forgoing an optimization to give you the last element in o(1) time and traverses the structure
Well, the answer to this is the peek function
And mostly you know you're working with vectors so you can use the peek function directly.
of course
How about the original question then? Why doesn't defn implicitly name the inner function?
i believe the decision was made to make it a recur target so the non-stack consuming loop was obvious rather than implicit. makes it more uniform. unless you are just talking about skipping the var indirection?
I'm talking about skipping the var indirection
this is valid and would break i guess?
(defn f
[x]
(if (zero? x)
:done
(with-redefs [f (fn [y] :exit-early)]
(f x))))(f 1) for me terminate, for you would be an infinite loop
I've also crafted a code that would break
that would break under your proposed change?
But, I mean, who would ever even think about writing code like this without having the only intention to break it?
dunno. but an optimization to shave negligible time causes semantic issues isn’t a great starting point i think
Like sure, we just showcased a quick example that would break it, but is there really a single line of Clojure code that relies on this?
With a different intention other than breaking the code
one thing to check? if you compile with direct linking do you get the performance gain you are advocating for? Perhaps this is even already a knob you control
Like, why would anyone ever try redefining a code that is currently being executed? Who would ever write a recursive function that does not rely on itself, but would rather call a completely difrferent implementation of some bad guy decides to change it?
(defn foo
[x]
(letfn [(foo [y] (println "shadowd by letfn") :letfn)]
(let [foo (fn [x] (println "shadowed by let") :let)]
(foo x))))
what does this return under your optimization?(defmacro defn-2 [name & fdecl]
`(def ~name (fn ~name ~@fdecl)))
(defn foo
[x]
(letfn [(foo [y] (println "shadowd by letfn") :letfn)]
(let [foo (fn [x] (println "shadowed by let") :let)]
(foo x))))
=> #'mila-lang.core/foo
(defn-2 foo-2
[x]
(letfn [(foo [y] (println "shadowd by letfn") :letfn)]
(let [foo (fn [x] (println "shadowed by let") :let)]
(foo x))))
=> #'mila-lang.core/foo-2
(foo 12)
shadowed by let
=> :let
(foo-2 12)
shadowed by let
=> :letWhat do you mean by direct linking?
direct linking i think can remove the var indirection
Oh, ok, this is very interesting...
Thanks for the reference! This indeed solves this issue!
excellent!
btw, a fun fact:
(fn fib [n]
(case n
0 0
1 1
(+ (fib (dec n))
(fib (- n 2)))))
does in fact call itself directly, because fib in this case is the fn instance (verified by decompiling)., and this should even work across arities.
The root cause of this not happening on a defn is that, ignoring docs and meta, defn basically destructures its contents as [name & fn-body] and expands into (def fn object not knowing about itself.
TCO was deliberately left out to avoid random stack overflows on would-be tail calls that in fact aren't. I feel that recur is a better idea either way, it's more recognizable, and makes you think twice before calling self. Not that raw recursion is particularly common in your usual Clojure code, at any rate.This was a surprising finding - symbols implement clojure.lang.IFn - is this by design? Is it because symbols can be map keys and have to be support this syntax
(def a-map {'foobar :test})
('foobar a-map) ;=> :test
It tripped me up because I need to accept either a qualified symbol (to pass to requiring-resolve) or a function as an argument - so I can't use ifn?` on it's own to check if the inputs are valid.Would using fn? instead of ifn? work for you in this case? (FWIW, I would have reached for fn? by default)
It's by design, yes - symbols, just like keywords, look themselves up.
@grzm it would, but as it happens sometimes the code gets #' passed and that doesn't work with fn?
You could go the other way around - symbol? (maybe even qualified-symbol?) with an (assert (ifn? ...)) in the "else" branch.
Yes, that was the solution in the end - but it took a few minutes to figure out. Thank you all
#' is a var, not a symbol. Maybe it's worth checking to see if it's derefable and see if the thing inside the container is a fn??
are there any open issues for clojure to add multiple bindings to if-let when-let if-some when-some?
EDIT: vote here https://ask.clojure.org/index.php/3786/allow-multiple-bindings-for-if-let-when-let-some-and-when-some
You can vote for the Ask: https://ask.clojure.org/index.php/3786/allow-multiple-bindings-for-if-let-when-let-some-and-when-some
now we have 2 votes 🙂
better-cond uses multiple bindings for when-let and others, and I’ve seen people here mention having a custom when-lets, and we have one in our codebase too.
I've added my vote too 🙂 Oh, and I have a similar when-let macro in my codebase(s) too 🙂