Had an unwelcome surprise with inline functions th...
# announcements
m
Had an unwelcome surprise with inline functions the other day. I had a custom inline function with multiple branches that would execute a functional parameter. I didn't realize until looking at the bytecode that functional parameter arguments were actually written out once each time that the functional param is referenced. E.g.:
Copy code
inline fun myInlineFun(myCase: Boolean, crossinline doSomething: () -> Unit) {
    if (myCase) {
        doSomething()
    } else {
        println("Start")
        doSomething()
        println("End")
    }
}
The argument for the
doSomething
param is written out twice in the resulting bytecode. Logically, this makes perfect sense to me -- unless there were a simple way for the compiler to remove the branches (which it does actually do if the condition is trivial, by the way), then the function must be written twice. It can have dangerous implications for uses though, especially if a caller attempts to use a long functional param. I didn't notice a kotlinc or IntelliJ warning/inspection for this. Not sure if there are any other tools which would provide one. Anybody else run into this?
z
The main use case for inline functions is exactly this behavior – avoiding allocating an object for lambdas for functions that do things like this:
Copy code
inline fun DB.withTransaction(block: Transaction.() -> Unit) {
  startTransaction()
  try {
    block()
    commitTransaction()
  finally {
    rollbackTransaction()
  }
}
You can use
noinline
on your function parameters to prevent them from being inlined.
m
Thanks, @Zach Klippenstein (he/him) [MOD]. I'm referring only to the case where the functional parameter is referenced multiple times in the function. I don't believe that there's any bug here, but it seems atypical (and dangerous) to me that callers of inline methods would want the bytecode of their functional argument written out multiple times. For example, the author should clearly optimize the following function to use only a single
block()
call, particularly to guard against cases where
block
is large, but this could easily be missed:
Copy code
inline fun DB.withTransaction(block: Transaction.() -> Unit) {
    if (!isTransactionAware) {
        val t = startTransaction()
        t.block()
        t.commitTransaction()
        return
    }
    val t = startTransaction()
    try {
        t.block()
        t.commitTransaction()
    } finally {
        t.rollbackTransaction()
    }
}
The object allocation avoidance would still yield a runtime benefit. However, the resulting class size for callers may become much greater than initially expected since the functional argument is now duplicated. Take this usage example:
Copy code
fun executeCriteria(db: DB) {
    db.executeInTransaction {
        // ... many criteria instructions ...
    }
}
The compiled output would be equivalent to:
Copy code
void executeCriteria(DB db) {
    if (!db.isTransactionAware) {
        Transaction t = db.startTransaction();
        // ... many criteria instructions ...
        t.commitTransaction();
        return
    }
    Transaction t = db.startTransaction();
    try {
        // ... many criteria instructions ...
        t.commitTransaction();
    } finally {
        t.rollbackTransaction();
    }
}
I'm wondering if it would make sense to include a compiler warning in these cases.
k
Inlining code (almost) always increases the code size anyway, but I do see your point as nested inlines could exponentially increase the generated code.