添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

The Kotlin compiler is generating unused variables due to dead code elimination.

I’m developing a performance-sensitive application where I’m trying to achieve robustness by declaring all assumptions but without impacting the run-time performance. These assumptions are enabled and verified when running tests but disabled when deploying the application for production. Not to be confused with validations, these are the type of assumptions that programmers make which must be true regardless of user input or environment (so regular validation remains).

Unlike regular assertions, it’s important that these assumptions get stripped out at compile time so that I get the following benefits:

  • Reduced class size reduces class loading time & hotspot compiler time
  • Reduced method size affects eligibility for inlining & escape analysis by the JVM (which enable other follow-on optimizations)
  • There are also other minor improvements such as avoiding the predictable check whether assertions are enabled etc.
  • Here’s a sample Kotlin file to illustrate the use-case:

    const val VERIFY_ASSUMPTIONS = false //deploying to production

    fun generateValueInBounds(lowerBound: Int, upperBound: Int): Int {
    assumeTrue(lowerBound <= upperBound)
    return lowerBound //fake implementation for demo

    inline fun assumeTrue(value: Boolean) {
    if (VERIFY_ASSUMPTIONS) {
    assert(value)

    So the compiler inlined the assumeTrue function and correctly eliminated the dead code (because VERIFY_ASSUMPTIONS is false) but the first function generates an unused variable:

    boolean var10000 = lowerBound <= upperBound;

    This was the variable that it expected to pass into the assumeTrue function.

    This common scenario (for my project) seems to have been missed by the dead-code elimination logic.

    Maybe Kotlin compiler isn’t so clever as what you expected to be.

    After compile, generated bytecode of method assumeTrue is

    public static final void assumeTrue(boolean);
        Code:
           0: nop
           1: return
    

    This is a empty method.

    If you delete the inline of method assumeTrue, generated bytecode could be

    public static final int generateValueInBounds(int, int);
        Code:
           0: iload_0
           1: iload_1
           2: if_icmpgt     9
           5: iconst_1
           6: goto          10
           9: iconst_0
          10: invokestatic  #13                 // Method assumeTrue:(Z)V
          13: iload_0
          14: ireturn
    

    After inline keyword is added, generateValueInBounds’s bytecode is simply changed:

      public static final int generateValueInBounds(int, int);
        Code:
           0: iload_0
           1: iload_1
           2: if_icmpgt     9
           5: iconst_1
           6: goto          10
           9: iconst_0
          10: istore_2
          11: nop
          12: nop
          13: iload_0
          14: ireturn
    

    So, it clear that inline’s usage is just to tell Kotlin compiler to replace the bytecode invoke* where invokes the function, you shouldn’t expect to use inline to improve performance without lambda params.
    Two correct usages of inline are something like below:

    inline fun <T> repeat(times: Int, action: (T) -> Unit) {
        require(times >= 0) { "times < 0: $times" }
        for(i in 0 until times) action(i)
    public inline fun <reified @PureReifiable T> arrayOf(vararg elements: T): Array<T>
    

    This is why you will get a warning when compile your code:

    Warning:(8, 1) Kotlin: Expected performance impact of inlining ‘public inline fun assumeTrue(value: Boolean): Unit defined in root package in file Main.kt’ can be insignificant. Inlining works best for functions with lambda parameters

    boolean var10000 = lowerBound <= upperBound;

    To be able to eliminate this expression the compiler must know that the property getters and the comparison do not produce any side effects, otherwise it could change the program behavior.

    I think, you achieve the desired code elimination with the help of inline assumeTrue function, but in a slightly different way:

    inline fun assumeTrue(value: () -> Boolean) {
        if (VERIFY_ASSUMPTIONS) {
            assert(value.invoke())
    fun generateValueInBounds(lowerBound: Int, upperBound: Int): Int {
        assumeTrue { lowerBound <= upperBound }
        return lowerBound
    

    Here the value being asserted is computed in a lambda, that is passed to assumeTrue and the block inside the lambda could be eliminated entirely, because it is inlined into the conditional branch.

    Thanks,

    Although functions with lambda’s benefit most from inlining, implying that all other usages of inlining are not correct isn’t right since it actually compiles and works correctly. The Kotlin compiler could have prevented inlining regular functions so I think it’s alluding to the fact that inlining large functions could have negative impacts.

    My usage of inline is mostly intended for code elimination (and of course lambdas). I also simplified my example so the assumeTrue function has a second parameter taking a lambda that generates the error message (which gets passed into assert). Since the first parameter isn’t a lambda, it gets generated as an unused variable that was intended for the eliminated inlined function.

    Passing the Boolean parameter as a lambda is a nice trick that solves this problem.

    It would be nice if the compiler realized that the comparison had no side effects as a future enhancement.

    Thanks for your help

    Strictly speaking, we can eliminate unused expressions after inilning. There’re some cases that we don’t support yet (as the one mentioned above). https://youtrack.jetbrains.com/issue/KT-20569.

    However, the scope of what can be done on that level is rather limited, because many instructions that look like “pure” (such as, for example, getting a value of a simple top-level val) actually have side effects (corresponding class could be loaded, and related static initialization is performed). Keep that in mind when writing performance-critical code.

    determinant:

    So the compiler inlined the assumeTrue function and correctly eliminated the dead code (because VERIFY_ASSUMPTIONS is false)

    Just wanted to point out that the Kotlin compiler did not optimize away dead code based on const vals until version 1.1.4, so if using an older version that would not be optimized away. See https://youtrack.jetbrains.com/issue/KT-17007

    Also just because things appear in byte code does not mean they actually affect performance. Byte code gets translated to machine code by the VM and it can also optimize away lots of this sort of stuff. I recently did some deep diving into this process on Android where byte code is translated to dex code and on the device dex is translated to machine code by ART. From what I saw on Android this process was very good at throwing away unnecessary code. I would assume that regular Java is also as good.

    dalewking:

    Also just because things appear in byte code does not mean they actually affect performance.

    I just wanted to set the record straight for others that might read this in the future. Extra useless statements in the byte code actually do affect performance on several levels:

  • Larger class size affects class loading performance.
  • Performance while executing in interpreted mode is worse (eg. for the first 10,000 invocations with default settings).
  • Larger method size affects JIT compile time when the method gets optimized.
  • The JIT compiler doesn’t even consider inlining a method if it exceeds a certain number of bytes.
  • The JIT compiler doesn’t even consider performing escape analysis on a method if it exceeds a certain number of bytes. Escape analysis can provide a huge performance improvement.
  • Optimizations 4 & 5 enable additional follow-on optimizations. So additional useless statements that push the method size above the thresholds do in fact have a noticeable impact.
  • I’m using Kotlin 1.1.4 and very much enjoying it so far.

    Thanks

    i stand by my statement other than I should have inserted the word “necessarily” which is compatible with both our statements:

    Also just because things appear in byte code does not necessarily mean they actually affect performance.

    I do not deny that they can affect performance. But in many cases they may be immeasurable.

    I personally would like to see the Kotlin compiler implement more optimizations. I don’t really want to rely on the JIT to do so for me, and in certain cases Kotlin can do it more easily. I know that the Java compiler doesn’t really optimize at all, but at language level is much closer to the JVM. Then again, I like the C++ idea of ‘zero overhead abstraction’ that Kotlin doesn’t quite provide with certainty (too much depends on a JVM that isn’t Kotlin optimized). In particular delegate properties are something I’d like to see getting “value type” treatment where appropriate.