javakotlinconstantsimmutabilitykotlinc

Will immutable objects with const parameters be optimized to be instantiated only once by the Kotlin compiler


There are many immutable classes in Java like String and primitive wrapper classes, and Kotlin introduced many others like Range subclasses and immutable Collection subclasses.

For iterating Ranges, from Control Flow: if, when, for, while - Kotlin Programming Language we already know:

A for loop over a range or an array is compiled to an index-based loop that does not create an iterator object.

However in other scenarios when dealing with Ranges this optimization is not possible.

When creating such immutable classes with const parameters, or more generally, recursively with const parameters, instantiating the class only once will bring performance gains. (In other words, if we call this a const immutable instantiation, an instantiation is a const immutable instantiation if and only if all its parameters are either constants or const immutable instantiations.) Since the Java compiler doesn't have a mechanism to know whether a class is immutable, does the Kotlin compiler optimize such classes to be instantiated only once, based on its knowledge of its known immutable classes?

For a more specific example of application, consider the following code:

repeat(1024) {
    doSomething(('a'..'z').random())
}
val LOWERCASE_ALPHABETS = 'a'..'z'
repeat(1024) {
    doSomething(LOWERCASE_ALPHABETS.random())
}

Would the second one bring any performance improvements?


Solution

  • I think the best thing you can do is to check what instructions the compiler generates.

    Let's take the following source code:

    fun insideRepeat() {
        repeat(1024) {
            doSomething(('a'..'z').random())
        }
    }
    
    fun outsideRepeat() {
        val range = 'a'..'z'
        repeat(1024) {
            doSomething(range.random())
        }
    }
    

    For insideRepeat it will generate something like (I added a few comments):

        public final static insideRepeat()V
        L0
        LINENUMBER 2 L0
        SIPUSH 1024
        ISTORE 0
        L1
        L2
        ICONST_0
        ISTORE 1
        ILOAD 0
        ISTORE 2
        L3
        ILOAD 1
        ILOAD 2
        IF_ICMPGE L4 // loop termination condition
        L5
        ILOAD 1
        ISTORE 3
        L6
        ICONST_0
        ISTORE 4
        L7 // loop body
        LINENUMBER 3 L7
        BIPUSH 97
        ISTORE 5
        NEW kotlin/ranges/CharRange
        DUP
        ILOAD 5
        BIPUSH 122
        INVOKESPECIAL kotlin/ranges/CharRange.<init> (CC)V // new instance created inside the loop
        INVOKESTATIC FooKt.random (Lkotlin/ranges/CharRange;)Ljava/lang/Object;
        INVOKESTATIC FooKt.doSomething (Ljava/lang/Object;)Ljava/lang/Object;
        POP
    

    While for the outsideRepeat it will generate:

    public final static outsideRepeat()V
    L0
    LINENUMBER 8 L0
    BIPUSH 97
    ISTORE 1
    NEW kotlin/ranges/CharRange
    DUP
    ILOAD 1
    BIPUSH 122
    INVOKESPECIAL kotlin/ranges/CharRange.<init> (CC)V // range created outside loop
    ASTORE 0
    L1
    LINENUMBER 9 L1
    SIPUSH 1024
    ISTORE 1
    L2
    L3
    ICONST_0
    ISTORE 2
    ILOAD 1
    ISTORE 3
    L4
    ILOAD 2
    ILOAD 3
    IF_ICMPGE L5 // termination condition
    L6
    ILOAD 2
    ISTORE 4
    L7
    ICONST_0
    ISTORE 5
    L8
    LINENUMBER 10 L8
    ALOAD 0
    INVOKESTATIC FooKt.random (Lkotlin/ranges/CharRange;)Ljava/lang/Object;
    INVOKESTATIC FooKt.doSomething (Ljava/lang/Object;)Ljava/lang/Object;
    POP
    

    So it seems like the second version brings performance improvements indeed (also considering the GC will need to deallocate less objects)