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 Range
s, 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 Range
s 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?
The const modifier in Kotlin is used for compile-time constants. Immutability is done with a val keyword. You don't have to hide or delete somehow any setters of val properties as such properties don't have setters.
What does Immutable mean? By definition, immutable means that, once created, an object/variable can't be changed. So, instead of changing a property of an object, you have to make a copy (or clone) of the entire object and in the process, change the property in question.
An important invariant that Kotlin/Native runtime maintains is that the object is either owned by a single thread/worker, or it is immutable (shared XOR mutable). This ensures that the same data has a single mutator, and so there is no need for locking to exist.
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)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With