Exploring Kotlin’s hidden costs — Part 2
Local functions, null safety and varargs
This is part 2 of an ongoing series about the Kotlin programming language. Don’t forget to read part 1 if you haven’t already.
Let’s take a new look behind the curtain and discover the implementation details of more Kotlin features.
Local functions
There is a kind of function we did not cover in the first article: functions that are declared inside other functions, using the regular syntax. These are called local functions and they are able to access the scope of the outer function.
fun someMath(a: Int): Int {
fun sumSquare(b: Int) = (a + b) * (a + b)
return sumSquare(1) + sumSquare(2)
}
Let’s begin by mentioning their biggest limitation: local functions can not be declared inline
(yet?) and a function containing a local function can not be declared inline
either. There is no magical way to avoid the cost of function calls in this case.
After compilation, these local functions are converted to Function
objects, just like lambdas and with most of the same limitations described in the previous article regarding non-inline functions. The Java representation of the compiled code looks like this:
public static final int someMath(final int a) {
Function1 sumSquare$ = new Function1(1) {
// $FF: synthetic method
// $FF: bridge method
public Object invoke(Object var1) {
return Integer.valueOf(this.invoke(((Number)var1).intValue()));
}
public final int invoke(int b) {
return (a + b) * (a + b);
}
};
return sumSquare$.invoke(1) + sumSquare$.invoke(2);
}
There is however one less performance hit compared to lambdas: because the actual instance of the function is known from the caller, its specific method will be called directly instead of its generic synthetic method from the Function
interface. This means that no casting or boxing of primitive types will occur when calling a local function from the outer function. We can verify this by looking at the bytecode:
ALOAD 1
ICONST_1
INVOKEVIRTUAL be/myapplication/MyClassKt$someMath$1.invoke (I)I
ALOAD 1
ICONST_2
INVOKEVIRTUAL be/myapplication/MyClassKt$someMath$1.invoke (I)I
IADD
IRETURN
We can see that the method being invoked twice is the one accepting an int
and returning an int
, and that the addition is performed immediately without any intermediate unboxing operation.
Of course there is still the cost of creating a new Function
object during each method call. This can be avoided by rewriting the local function to be non-capturing:
fun someMath(a: Int): Int {
fun sumSquare(a: Int, b: Int) = (a + b) * (a + b)
return sumSquare(a, 1) + sumSquare(a, 2)
}
Now the same Function
instance will be reused an still no casting or boxing will occur. The only penalty of this local function compared to a classic private function will be the generation of an extra class with a few methods.
Local functions are an alternative to private functions with the added benefit of being able to access local variables of the outer function. That benefit comes with the hidden cost of the creation of a
Function
object for each call of the outer function, so non-capturing local functions are preferred.
Null safety
One of the best features of the Kotlin language is that it makes a clear distinction between nullable and non-null types. This enables the compiler to effectively prevent unexpected NullPointerException
s at runtime by forbidding any code assigning a null
or nullable value to a non-null variable.
Non-null arguments runtime checks
Let’s declare a public function taking a non-null String
as argument:
fun sayHello(who: String) {
println("Hello $who")
}
And now take a look at the Java representation of the compiled code:
public static final void sayHello(@NotNull String who) {
Intrinsics.checkParameterIsNotNull(who, "who");
String var1 = "Hello " + who;
System.out.println(var1);
}
Notice that the Kotlin compiler is a good Java citizen and adds the @NotNull
annotation to the argument, so Java tools can use this hint to show a warning when a null
value is passed.
But an annotation is not enough to enforce null safety from the external callers. That’s why the compiler also adds at the very beginning of our function a static method call that will check the argument and throw an IllegalArgumentException
if it’s null
. The function will fail early and consistently rather than failing randomly later with a NullPointerException
, in order to make the unsafe caller code easier to fix.
In practice, every public function has one static call to Intrinsics.checkParameterIsNotNull()
added for each non-null reference argument. These checks are not added to private functions because the compiler guarantees that the code inside a Kotlin class is null safe.
The performance impact of these static calls is negligible and they are really useful when debugging and testing an app. That being said, you may see them as an unnecessary extra cost for release builds. In that case, it’s possible to disable runtime null checks by using the -Xno-param-assertions
compiler option or by adding the following ProGuard rule:
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
static void checkParameterIsNotNull(java.lang.Object, java.lang.String);
}
Note that this ProGuard rule will only take effect with optimizations enabled. Optimizations are disabled in the default Android ProGuard configuration.
Nullable primitive types
This seems obvious but needs to be reminded: a nullable type is always a reference type. Declaring a variable for a primitive type as nullable prevents Kotlin from using the Java primitive value types like int
or float
and instead the boxed reference types like Integer
or Float
will be used, involving the extra cost of boxing and unboxing operations.
Contrary to Java which allows you to be sloppy and use an Integer
variable almost exactly like an int
variable, thanks to autoboxing and disregard of null safety, Kotlin forces you to write safe code when using nullable types so the benefits of using non-null types become clearer:
fun add(a: Int, b: Int): Int {
return a + b
}fun add(a: Int?, b: Int?): Int {
return (a ?: 0) + (b ?: 0)
}
Use non-null primitive types whenever possible for more readable code and better performance.
About arrays
There are 3 types of arrays in Kotlin:
IntArray
,FloatArray
and others: an array of primitive values.
Compiles toint[]
,float[]
and others.Array<T>
: a typed array of non-null object references.
This involves boxing for primitive types.Array<T?>
: a typed array of nullable object references.
This also involves boxing for primitive types, obviously.
If you need an array for a non-null primitive type, prefer using
IntArray
thanArray<Int>
for example, to avoid boxing.
Varargs
Kotlin allows to declare functions with a variable number of arguments, like Java. The declaration syntax is a bit different:
fun printDouble(vararg values: Int) {
values.forEach { println(it * 2) }
}
Just like in Java, the vararg
argument actually gets compiled to an array argument of the given type. You can then call these functions in three different ways:
1. Passing multiple arguments
printDouble(1, 2, 3)
The Kotlin compiler will transform this code to a creation and initialization of a new array, exactly like the Java compiler does:
printDouble(new int[]{1, 2, 3});
So there is the overhead of the creation of a new array, but this is nothing new compared to Java.
2. Passing a single array
This is where things differ. In Java, you can directly pass an existing array reference as vararg argument. In Kotlin, you need to use the spread operator:
val values = intArrayOf(1, 2, 3)
printDouble(*values)
In Java, the array reference is passed “as-is” to the function, with no extra array allocation. However, the Kotlin spread operator compiles differently, as you can see in this Java representation:
int[] values = new int[]{1, 2, 3};
printDouble(Arrays.copyOf(values, values.length));
The existing array always gets copied when calling the function. The benefit is safer code: it allows the function to modify the array without impacting the caller code. But it allocates extra memory.
Note that calling a Java method with a variable number of arguments from Kotlin code has the same effect.
3. Passing a mix of arrays and arguments
The main benefit of the spread operator is that it also allows mixing arrays with other arguments in the same call.
val values = intArrayOf(1, 2, 3)
printDouble(0, *values, 42)
How does this get compiled? The resulting code is quite interesting:
int[] values = new int[]{1, 2, 3};
IntSpreadBuilder var10000 = new IntSpreadBuilder(3);
var10000.add(0);
var10000.addSpread(values);
var10000.add(42);
printDouble(var10000.toArray());
In addition to the creation of a new array, a temporary builder object is used to compute the final array size and populate it. This adds another small cost to the method call.
Calling a function with a variable number of arguments in Kotlin adds the cost of creating a new temporary array, even when using values from an existing array. For performance-critical code where the function is called repeatedly, consider adding a method with an actual array argument instead of
vararg
.
Thank you for reading and please share this article if you liked it.
Keep reading by heading to part 3: delegated properties and ranges.