具有 const 参数的不可变对象是否会被优化为仅由 Kotlin 编译器实例化一次

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

Java 中有许多不可变的 classes,例如 String 和原始包装器 classes,Kotlin 引入了许多其他的 Range subclasses 和不可变 Collection subclasses.

迭代Ranges,从Control Flow: if, when, for, while - Kotlin Programming Language我们已经知道:

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

然而,在处理 Ranges 的其他场景中,这种优化是不可能的。

当使用 const 参数创建这样的不可变 classes 时,或者更一般地,使用 const 参数递归地,只实例化 class 一次将带来性能提升。 (换句话说,如果我们称其为 const 不可变实例化,则实例化是一个 const 不可变实例化当且仅当其所有参数都是常量或 const 不可变实例化。)因为 Java 编译器没有知道 class 是否不可变的机制,Kotlin 编译器是否根据其已知的不可变 classes 的知识优化此类 classes 使其仅实例化一次?

有关更具体的应用示例,请考虑以下代码:

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

第二个会带来任何性能改进吗?

我认为你能做的最好的事情就是检查编译器生成的指令。

我们来看下面的源代码:

fun insideRepeat() {
    repeat(1024) {
        doSomething(('a'..'z').random())
    }
}

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

对于 insideRepeat 它将生成类似的东西(我添加了一些评论):

    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

而对于 outsideRepeat 它将生成:

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

所以看起来第二个版本确实带来了性能改进(同时考虑到 GC 将需要释放更少的对象)