Kotlin λ and auto-boxing of parameters and result

For example i have this code (not real example, but very close):

fun makeMagicAlgorithm(factor: Int): (Int, Int) -> Int {
  return { a, b -> a + b * factor }
}

val f = makeMagicAlgorithm(100)
val result = (0..100000).reduce(f)

and on every call f() JVM will allocate in heap three objects
In my android game it’s very expensive use GC (especially on Dalvik).

What can i do, to avoid this?

Right now I only see two way:

  1. Use NOT generic interface and anonymous class (java style)
  2. Try to inline this function

As you probably know, the heap allocations are due to boxing and unboxing, which is due to generics.

Getting rid of the boxing/generics in the code you provided requires both inlining and defining specialised (non-generic) versions of the functions.

You might find that your particular case can be refactored so that all you need is an inline function. If you need or want a more general solution, then read on …

There is generics/boxing in two different places in your example:

  1. the return type of makeMagicAlgorithm (which at runtime is an instance of Function2<T,T,T>)
  2. the implementation of reduce (which operates on an instance of Iterable)

As you may have already realised, part of the problem is that the JVM does not support using primitives for generic classes (although this is currently being implemented for release sometime after Java 9). The parameterised types (T’s above) must be Integer at runtime and cannot be int - thus the boxing and heap allocations.

You can address 1) by defining a specialised function type to use in place of (Int,Int)->Int (e.g. an interface called IntBinaryOperator).

You can address 2), you can provide a new implementation of the reduce extension function that takes an IntBinaryOperator.

Assuming you had defined the interface and function described above,
you could then write the following and avoid the boxing and heap allocations at runtime:

fun makeMagicAlgorithm(factor: Int): IntBinaryOperator {
    return object: IntBinaryOperator {
        override fun invoke(a: Int, b: Int): Int {
            return a + b * factor
        }
    }
}

val f = makeMagicAlgorithm(100)
val result = (0..1000000).reduce(f)

When I tested this code, it ran more than twice as fast when the original code didn’t trigger a GC.
It ran 3 to 4 times as fast when the original code did trigger a GC.
Clearly the boxing and unboxing involves overheads beyond that of GC.
Note that I ran these tests on the hotspot VM (not android).

Here’s the definitions related to IntBinaryOperator that I used to test the above:

// Int specialisation of Function2<T,T,T>
public interface IntBinaryOperator { public operator fun invoke(a: Int, b: Int): Int }

// Int specialisation of Iterable<T>.reduce ...
public fun IntRange.reduce(operation: IntBinaryOperator): Int {
    val iterator = this.iterator()
    if (!iterator.hasNext()) throw UnsupportedOperationException("Empty collection can't be reduced.")
    var accumulator: Int = iterator.nextInt()
    while (iterator.hasNext()) {
        accumulator = operation(accumulator, iterator.nextInt())
    }
    return accumulator
}

Note that there is no point inlining this implementation of reduce because it does not involve a generic function. If you make it inline, the kotlin compiler warns about this saying “warning: expected performance impact of inlining ‘…’ can be insignificant. Inlining works best for functions with lambda parameters”.

2 Likes