When using enums and R8…

Kotlin Vocabulary — switching on enums, and R8 optimization

Chet Haase
Android Developers

--

Whenever you’re using or learning a new language, it’s important to understand features are available in the language and whether there’s any overhead associated with those features.

This gets particularly interesting in Kotlin, because it offers features that are not in the Java programming language, even though it compiles down to Java bytecode. How does that work? And is there overhead associated with any of those features? And if there is overhead, is there anything that we can do about it?

This article is about enums and when statements (switch statements in Java). I’ll talk about some of the non-obvious overhead associated with when and about how you can use the Android R8 compiler to optimize your app and reduce that overhead.

Compilers

First, let’s talk about D8 and R8.

There are actually three compiler steps that kick in for the Kotlin code that you write for Android applications.

#1 The Kotlin Compiler

First of all, the Kotlin compiler runs and converts your code into bytecode for the Java programming language. Which is great, except… we don’t run that bytecode on Android devices. Instead, we run something called DEX, which stands for Dalvik Executable. Dalvik was the name of the original runtime on Android. The current runtime, since Android 5.0 Lollipop, is called ART, which stands for Android RunTime… but ART still runs the same DEX code (it would have been silly to replace the runtime with something that didn’t handle the old executable, wouldn’t it?)

#2 D8

D8 is the second compiler in the chain, translating from Java bytecode into DEX code. At this point, you have code that you could run on Android. Or, you could optionally use a third compiler, called R8.

#3 R8 (optional, but recommended)

R8 is an extended mode of D8 that is used to optimize and shrink applications. Basically, it’s is a replacement for ProGuard. Because R8 runs as part of the D8 compilation step, it is able to be faster than ProGuard, which is a separate compiler altogether.

But R8 is not enabled by default, so if you want to use it (which you may for some of the optimizations discussed here) you need to enable it. Set minifyEnabled = true in your application’s build.gradle file to force R8 to run. This will happen after all that other compilation to make sure that you get a shrunk and optimized app.

android {
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile(
‘proguard-android-optimize.txt’),
‘proguard-rules.pro’
}
}
}

Enums

Now, let’s talk about enums.

The overhead and functionality of enums is essentially the same whether you’re writing in Java or Kotlin code, but it’s interesting what we can do about some of that overhead once we introduce R8 into the mix.

Enums by themselves don’t have any overhead that is non-obvious. If you’re using an enum in Kotlin, that’s just going to translate to an enum in the Java programming language. And it’s not a big deal. (Yes, we used to talk about avoiding enums… but that was many years and an entire runtime ago — enums are fine).

The overhead kicks in when you use when statements.

First of all, let’s look at a sample enum:

enum class BlendMode {
OPAQUE,
TRANSPARENT,
FADE,
ADD
}

Here we have four values for our enum. It doesn’t really matter what those are; it’s just an example.

Enums + When

Now let’s add a when statement, switching on that enum:

fun blend(b: BlendMode) {
when (b) {
BlendMode.OPAQUE -> src()
BlendMode.TRANSPARENT -> srcOver()
BlendMode.FADE -> srcOver()
BlendMode.ADD -> add()
}
}

For each one of the enum values, we’re going to call another function.

If you take a look at what it’s compiled down to in Java bytecode (which you can see in Android Studio by looking at bytecode directly (Tools -> Kotlin -> Show Kotlin Bytecode), then clicking on the “Decompile” button), it looks something like this:

public static void blend(@NotNull BlendMode b) {
switch (BlendingKt$WhenMappings.
$EnumSwitchMapping$0[b.ordinal()]) {
case 1: {
src();
break;
}
// ...
}
}

Instead of doing a switch on the enum’s value directly, it calls into this array. Where did that array come from?

And the array is stashed in this generated class file. Where did that class file come from?

What is going on here?

Auto-Generated Enum Mapping

It turns out that, for binary compatibility reasons, the code can’t simply switch on the ordinal value of the enum, because that is fragile. You could have a library that has an enum, and if you change the order of those items in the enum, you could break someone’s application, even though it looked the same in the code, they were just in a different order. But it was the implementation detail of the ordering that caused the breakage.

So instead, the compiler takes the ordinal value and maps it to another value, then no matter what you do with those enums, code that is built with that library will still run.

Of course, this means that something is going to get generated on your behalf. Or in this case, multiple somethings.

The generated code looks something like this:

public final class BlendingKt$WhenMappings {
public static final int[] $EnumSwitchMapping$0 =
new int[BlendMode.values().length];

static {
$EnumSwitchMapping$0[BlendMode.OPAQUE.ordinal()] = 1;
$EnumSwitchMapping$0[BlendMode.TRANSPARENT.ordinal()] = 2;
$EnumSwitchMapping$0[BlendMode.FADE.ordinal()] = 3;
$EnumSwitchMapping$0[BlendMode.ADD.ordinal()] = 4;
}
}

There’s that class BlendingKt$WhenMappings that gets generated. Inside of that is an array $EnumSwitchMapping$0 where the mappings are housed, and then some static code that actually performs the mapping operation.

This is the case when there is only one when statement. If we had more when statements, we would have another array for each of those statements, even when they use the same enum.

All of this overhead isn’t that big a deal. But it does mean that, unbeknownst to you, a class is being generated on your behalf, and then some number of arrays inside of that class, all of which takes more time at class-loading and instantiation time.

Fortunately, there’s something that we can do to reduce the overhead: That’s were R8 comes in.

R8 to the Rescue

R8 is an interesting optimizer. It can see everything in your application. It sees all of the code, whether it’s code that you wrote or library code that you’re building against. It can make decisions about optimizations knowing that, for example, it can avoid the overhead of these enum mappings. It does not need that mapping information, because it knows that the code is only going to use these enums in this certain way, so it can just call into the ordinal value instead.

This is what the R8-optimized code looks like when decompiled:

public static void blend(@NotNull BlendMode b) {
switch (b.ordinal()) {
case 0: {
src();
break;
}
// ...
}
}

It avoids generating the class and the mapping array, and just creates the optimal code that you wanted.

Check out R8, check out Kotlin, and enjoy writing better applications with Kotlin.

For More Information

Check out the following links for more information on these topics:

Jake Wharton goes into great detail on how d8 and r8 work, with specific examples of various features along with how to do things like run the compilers directly and how to get the decompiled results.

This article (and the video version of it) was taken from a longer presentation that I gave with Romain Guy at KotlinConf 2019. Check out the video for more information about Kotlin, language features, and optimizations.

--

--

Chet Haase
Android Developers

Past: Android development Present: Student, comedy writer Future: ???