Duplicate of#5537
Description
When generating code for a simple Enum, the compiler generates a huge chain of if-else-if
statements instead of a tableswitch. The same problem exists for more general ADTs of course (and it's more complicated to fix this for those since we would have to generate a hidden tag etc), but for just plain enums I would expect this to work. Everything is there for you - just detect this case (of a simple Enum), call ordinal() on it and index into the table. Even Kotlin does this.
Compiler version
"3.6.4"
Minimized code
enum Z:
case A, B, C, D, E, F, G
def f(n: Z): Int =
import Z.*
n match {
case A => 23
case B => 12
case C => -1
case D => 4
case E => 12
case F => -11
case G => 21
}
Output
public f(Lapp/Main$Z;)I
// parameter final n
L0
LINENUMBER 47 L0
ALOAD 1
ASTORE 2
L1
LINENUMBER 48 L1
GETSTATIC app/Main$Z$.A : Lapp/Main$Z;
ALOAD 2
ASTORE 3
DUP
IFNONNULL L2
POP
ALOAD 3
IFNULL L3
GOTO L4
L2
ALOAD 3
INVOKEVIRTUAL java/lang/Object.equals (Ljava/lang/Object;)Z
IFEQ L4
L3
BIPUSH 23
IRETURN
L4
LINENUMBER 49 L4
GETSTATIC app/Main$Z$.B : Lapp/Main$Z;
ALOAD 2
ASTORE 4
DUP
IFNONNULL L5
POP
ALOAD 4
IFNULL L6
GOTO L7
L5
ALOAD 4
INVOKEVIRTUAL java/lang/Object.equals (Ljava/lang/Object;)Z
IFEQ L7
L6
BIPUSH 12
IRETURN
L7
LINENUMBER 50 L7
GETSTATIC app/Main$Z$.C : Lapp/Main$Z;
ALOAD 2
ASTORE 5
DUP
IFNONNULL L8
POP
ALOAD 5
IFNULL L9
GOTO L10
L8
ALOAD 5
INVOKEVIRTUAL java/lang/Object.equals (Ljava/lang/Object;)Z
IFEQ L10
L9
ICONST_M1
IRETURN
L10
LINENUMBER 51 L10
GETSTATIC app/Main$Z$.D : Lapp/Main$Z;
ALOAD 2
ASTORE 6
DUP
IFNONNULL L11
POP
ALOAD 6
IFNULL L12
GOTO L13
L11
ALOAD 6
INVOKEVIRTUAL java/lang/Object.equals (Ljava/lang/Object;)Z
IFEQ L13
L12
ICONST_4
IRETURN
L13
LINENUMBER 52 L13
GETSTATIC app/Main$Z$.E : Lapp/Main$Z;
ALOAD 2
ASTORE 7
DUP
IFNONNULL L14
POP
ALOAD 7
IFNULL L15
GOTO L16
L14
ALOAD 7
INVOKEVIRTUAL java/lang/Object.equals (Ljava/lang/Object;)Z
IFEQ L16
L15
BIPUSH 12
IRETURN
L16
LINENUMBER 53 L16
GETSTATIC app/Main$Z$.F : Lapp/Main$Z;
ALOAD 2
ASTORE 8
DUP
IFNONNULL L17
POP
ALOAD 8
IFNULL L18
GOTO L19
L17
ALOAD 8
INVOKEVIRTUAL java/lang/Object.equals (Ljava/lang/Object;)Z
IFEQ L19
L18
BIPUSH -11
IRETURN
L19
LINENUMBER 54 L19
GETSTATIC app/Main$Z$.G : Lapp/Main$Z;
ALOAD 2
ASTORE 9
DUP
IFNONNULL L20
POP
ALOAD 9
IFNULL L21
GOTO L22
L20
ALOAD 9
INVOKEVIRTUAL java/lang/Object.equals (Ljava/lang/Object;)Z
IFEQ L22
L21
BIPUSH 21
IRETURN
L22
NEW scala/MatchError
DUP
ALOAD 2
INVOKESPECIAL scala/MatchError.<init> (Ljava/lang/Object;)V
ATHROW
L23
LOCALVARIABLE this Lapp/Main$; L0 L23 0
LOCALVARIABLE n Lapp/Main$Z; L0 L23 1
MAXSTACK = 3
MAXLOCALS = 10
Activity
som-snytt commentedon Jun 4, 2025
A long-standing request, which is to say, an old request for a contribution. Is GSOC set for this year?
odersky commentedon Jun 4, 2025
The Java version is described here: #5537 (comment). It uses a table switch, but is a lot more complex than a simple switch on ordinal. What exactly does Kotlin do? As I understand it, there's a trade-off between code simplicity and speed vs separate compilation guarantees.
nmichael44 commentedon Jun 5, 2025
@odersky @som-snytt Here is the same function in Kotlin and the code generated.
Generated code for
f
:odersky commentedon Jun 5, 2025
Thanks for reporting this! So Kotlin does use a simple table switch based on ordinal. Whereas Java jumps through a lot of hoops, involving one inner class for each enum switch, to ensure that reorderings of enums don't break binary compatibility.
Personally, I am not convinced binary compatibility under re-orderings is necessary here. Reordering an enum changes the
ordinal
values of enum constants. That's a semantic change, which should be done only if all clients can be inspected to not rely on precise ordinal values. So it seems strange to require a manual verification of client code but not require a recompile. Seen in another way, no public library should re-order enums, because clients could break. So why insist on that change being binary compatible?nmichael44 commentedon Jun 5, 2025
@odersky I agree with your reasoning. Doing this optimization is a good change.
He-Pin commentedon Jun 5, 2025
@nmichael44 @odersky maybe #23264 can help sometime?
And in Java 17 there is a https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/runtime/SwitchBootstraps.html#enumSwitch(java.lang.invoke.MethodHandles.Lookup,java.lang.String,java.lang.invoke.MethodType,java.lang.Object...)
som-snytt commentedon Jun 5, 2025
Kotlin does the same as Java. The
$EnumSwitchMapping$1
is the extra table that associates ordinal with "switch order".The static construction of that table is laborious but happens once.
My naive understanding is that it's a small price compared to a code comment
There is such a code comment to preserve "error ID" for compiler
ErrorMessageID
. The standard Josh Bloch advice is don't use ordinal for semantics. The JavaDoc is "Most programmers will have no use for this method." For that use case, it would be nice to have a mechanism that "preserves history", that is, generate a static table that preserves historical order (because we have git history) the way theSwitchMapping
preserves the order of a particular switch at a point in time.nmichael44 commentedon Jun 5, 2025
@odersky Here is the decompiled code for the Kotlin jar file. @som-snytt is correct that Kotlin does the same as Java, except that it's not a class per enum, it's one array per enum and one class per file (I had more enums in my kotlin file and all the arrays were constructed in the same class
WhenMappings
class -- see below).odersky commentedon Jun 5, 2025
That is there for a completely different reason. The authors of the file wanted to use an enum and ensure at the same time that the printed error number stays the same forever. It's certainly a dubious idiom, I was not a fan when it was introduced.
I am also very skeptical about not using ordinal in enums. If that's so bad, why is it not deprecated?
som-snytt commentedon Jun 5, 2025
There is no construct for "deprecated if you don't know what you're doing." A lint could ask, "Did you intend to use notify and wait?" (as an example of API that is not deprecated but is not intended for casual usage).