Welcome! Please see the About page for a little more info on how this works.

0 votes
in Compiler by

Given

(let [c (char 110)]
        (case c \n (println "lol")))

If I run decompile from https://clojars.org/com.clojure-goes-fast/clj-java-decompiler
I get the following output:

public final class json$fn__15103 extends AFunction {
public static final Object const__2;
public static final Var const__3;
public static final Var const__4;

public static Object invokeStatic() {
    final char G__15104;
    final char c = G__15104 = RT.charCast(110L);
    switch (Util.hash(G__15104)) {
        case 110: {
            if (Util.equiv((Object)G__15104, json$fn__15103.const__2)) {
                return ((IFn)json$fn__15103.const__3.getRawRoot()).invoke("lol");
            }
            break;
        }
    }
    throw new IllegalArgumentException((String)((IFn)json$fn__15103.const__4.getRawRoot()).invoke("No matching clause: ", G__15104));
}

@Override
public Object invoke() {
    return invokeStatic();
}

static {
    const__2 = 'n';
    const__3 = RT.var("clojure.core", "println");
    const__4 = RT.var("clojure.core", "str");
} 
}

My question is (given that this decompilation is correct) why does the compiler emit the if test, and more interestingly, is there a way to get rid of it (by means of instructing the compiler?).

It might also be that I should not care about this because the JIT will take care of it for me, but still.

Also, one could argue, I guess, that since we know that the thing I'm case'ing on is a char there would be no need to do a Util.hash on it?

1 Answer

0 votes
by

When looking for optimizations at this level of detail, i.e. JVM byte code and what the JIT does with it, it is probably best to read actual JVM byte code, rather than decompiled Java source code, which can in some cases be misleading. Even better would be to find alternate JVM byte code for which a popular recent version JVM JIT compiler produces faster code for based on performance results.

I know that can be a lot of tedious work to do, and I am definitely NOT saying "the current JVM byte code produced by the Clojure compiler is known to be optimal", but it sounds like you might be closer to the beginning of that process than the end.

It does seem true that the Clojure compiler could do fancier type-specific optimizations for case statements if it knows the type of the expression -- I doubt it ever does that today. I don't know what the JIT might do with this byte code, though, e.g. perhaps it would see that Util.hash is always being called with a Character object, and creating an in-lined version of Util.hash that checks whether the arg is a Character, returning a calculation tailored for that type if so, and otherwise calling the full Util.hash with all of its cases.

by
I dove into this after I wrote my question, and the answer lies much higher than at the jvm/emitter level, it's here:
https://github.com/clojure/clojure/blob/clojure-1.10.1/src/clj/clojure/core.clj#L6743

So if you do a `case` where all the tests are against ints, you get the fastest code:

```
(decompile (let [foo (int 3)]
             (case foo
               3 "bla")))
```
gives

```
    public static Object invokeStatic() {
        final int G__16058;
        final int foo = G__16058 = RT.intCast(3L);
        switch (G__16058) {
            case 3: {
                return "bla";
            }
            default: {
                throw new IllegalArgumentException((String)((IFn)json$fn__16057.const__3.getRawRoot()).invoke("No matching clause: ", G__16058));
            }
        }
    }
```
...