Lambda-Map Performance in Kotlin

Kotlin Bytecode

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:

  1. It could add up, depending on how many of .maps you use.
  2. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *