Lambda-Map Performance in Kotlin
Posted on November 19, 2018  (Last modified on December 27, 2022 )
3 minutes • 474 words
This project uses these versions of languages, frameworks, and libraries.
-
kotlin
: 1.2.60
This tutorial may work with newer versions and possibly older versions, but has only been tested on the versions mentioned above.
Working on an enterprise application written in Kotlin, I’ve recently noticed something that felt “off” to me. Perhaps you’ve seen it, perhaps you’ve written it — maybe even recently! It looks something like this (I call this a Lambda-Map):
object Demo {
fun main() {
listOf(1,2,3,4).map {
plusOne(it)
}
}
private fun plusOne(i: Int): Int = i + 1
}
Sure, this plusOne
function isn’t really necessary. We can just inline it in the Lambda, but imagine for just a moment that this function does much, much more than just add one to a number. It then feels just to have that code pulled out into a private function — but that’s not the offender here.
The offender here is the unnecessary lambda being used to call a private function in a map.
I feel like this happens due to the fact that Intellij’s default auto-complete for .map
is a new lambda. Rightfully so, it looks nice, shows off how great the language is, and likely is used the most often. But in the case of the private function that does all of the work, It’s not needed.
object Demo {
fun main() {
listOf(1,2,3,4).map(::plusOne)
}
private fun plusOne(i: Int): Int = i + 1
}
This is similar, but should avoid a few unnecessary calls. If you’re unfamiliar with the ::
syntax, fret not. These are called callableReference literals and can be used when you’d like to pass a named function as an argument. In this case, it negates the lambda passed into map completely, simply by directly calling the private function plusOne
. We can even break this simple example down and see small improvements in the bytecode generated from each of these.
Bytecode Break
Here is the generated bytecode for the Lambda example. This is only the bytecode for the map call, specifically, and not the entire object/file.
LINENUMBER 4 L6
GETSTATIC Demo.INSTANCE : LDemo;
ILOAD 6
INVOKESPECIAL Demo.plusOne (I)I
L7
L8
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
ASTORE 11
ALOAD 10
ALOAD 11
INVOKEINTERFACE java/util/Collection.add (Ljava/lang/Object;)Z
POP
And here’s the compiled bytecode for the callableReference literal.
LINENUMBER 4 L7
INVOKESPECIAL Demo.plusOne (I)I
L8
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
ASTORE 12
ALOAD 11
ALOAD 12
INVOKEINTERFACE java/util/Collection.add (Ljava/lang/Object;)Z
POP
Not only is it avoiding the GETSTATIC
instruction, but it also removes an unnecessary integer load (ILOAD
) into memory.
It’s certainly a micro-optimization on the performance scale, but:
- It could add up, depending on how many of
.map
s you use. - Personally, I think it makes the code cleaner and easier to read.
Anyways, that’s an interesting tidbit that I feel Intellij doesn’t help you with very much (It can’t do everything for you). Hopefully this was helpful!
If you’d like to learn more about Kotlin, you can check out my other Kotlin posts here !
“Bytecode Break” was borrowed from this fantastic talk by Huyen Tue Dao .