Inlining methods with kotlin.Number parameter

The case for inline functions is primarily to preserve performance, more specifically when one or more parameters are lambdas that would otherwise need to be wrapped in an object. As such, if a (non-reified) inline function is defined without any lambda parameters, the following warning is given:

warning: expected performance impact from inlining is insignificant. Inlining works best for functions with parameters of functional types

Such functions also provide the same performance benefit when one or more parameters is of type kotlin.Number, as seen below:

Non-inline method: Number is boxed.

private fun numToDouble(x: Number): Double {
    return x.toDouble()
}

fun main() {
    val double = numToDouble(2)
}
  private final static numToDouble(Ljava/lang/Number;)D
   L0
    LINENUMBER 6 L0
    ALOAD 0
    INVOKEVIRTUAL java/lang/Number.doubleValue ()D
    DRETURN
   L1
    LOCALVARIABLE x Ljava/lang/Number; L0 L1 0
    MAXSTACK = 2
    MAXLOCALS = 1

  public final static main()V
   L0
    LINENUMBER 10 L0
    ICONST_2
    INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;    // Primitive is boxed as `Integer` (subtype of `Number`)
    CHECKCAST java/lang/Number    // Checked conversion to `Number`
    INVOKESTATIC mathx/big/SrcKt.numToDouble (Ljava/lang/Number;)D
    DSTORE 0
   L1
    LINENUMBER 11 L1
    RETURN
   L2
    LOCALVARIABLE double D L1 L2 0
    MAXSTACK = 2
    MAXLOCALS = 2

inline method: boxing is avoided

private inline fun numToDouble(x: Number): Double {    // Warning is given here
    return x.toDouble()
}

fun main() {
    val double = numToDouble(2)
}
  // Definition of generic `numToDouble`

  // ...

  public final static main()V
   L0
    LINENUMBER 10 L0
    ICONST_2
    ISTORE 2    // Primitive is used directly
                // No type check needed
   L1
    ICONST_0
    ISTORE 3
   L2
    LINENUMBER 12 L2
    ILOAD 2
    I2D
   L3
    LINENUMBER 10 L3
    DSTORE 0
   L4
    LINENUMBER 11 L4
    RETURN
   L5
    LOCALVARIABLE x$iv I L1 L3 2
    LOCALVARIABLE $i$f$numToDouble I L2 L3 3
    LOCALVARIABLE double D L4 L5 0
    MAXSTACK = 2
    MAXLOCALS = 4

In the example given, the use of inline circumvents both primitive boxing and type checking. However, the compiler still complains that the performance gain is insignificant. The performance impact is even greater if the same function is inlined within code that is executed many times.

As a result, it is not logical that the warning should be given in this scenario. Maybe this can be changed in future versions of the language?

Sure, one may suppress the warning, but that is tedious when done repeatedly. One may also replace the Number parameters with type T given <reified T : Number>. However, this is not semantically consistent with the usage of reified type parameters (that is, to obtain the class instance of a generic type at runtime).

EDIT: Stumbling upon similar discussions, reified seems to be the accepted solution.

2 Likes

Just a quick note: while inline does have a performance aspect, IMHO the main point is language features like non-local returns, reified, etc.

We code for humans, not for computers.

If it was just about performance, then JVM inlining, an inlining annotation, compiler flags, other VMs like Graal, and AOT compiling would all fit the bill.

In Kotlin, even if inline decreased performance (which I expect it would in many cases), it still serves it’s main purpose of enabling language features that expect to operate directly at it’s call site.

2 Likes