StringBuilder and String Template Behavior


#1

I’m not sure if this is intended behavior, but let’s take the following Kotlin sample:

fun main(args: Array<String>) {
    val sb = StringBuilder() // first half
    sb.append("strings are cool ${sb.length}")

    val sb2 = StringBuilder() // second half
    sb2.append("strings are cool ").append(sb2.length)
}

The following bytecode for the JVM is then generated.

  // access flags 0x19
  public final static main([Ljava/lang/String;)V
    @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
   L0
    ALOAD 0
    LDC "args"
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V

   L1 // start of the first half
    LINENUMBER 4 L1
    NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    ASTORE 1
   L2
    LINENUMBER 5 L2
    ALOAD 1
    NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    LDC "strings are cool "
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    ALOAD 1
    INVOKEVIRTUAL java/lang/StringBuilder.length ()I
    INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    POP


   L3 // start of the second half
    LINENUMBER 7 L3
    NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    ASTORE 2
   L4
    LINENUMBER 8 L4
    ALOAD 2
    LDC "strings are cool "
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    ALOAD 2
    INVOKEVIRTUAL java/lang/StringBuilder.length ()I
    INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
    POP
   L5
    LINENUMBER 9 L5
    RETURN
   L6
    LOCALVARIABLE sb2 Ljava/lang/StringBuilder; L4 L6 2
    LOCALVARIABLE sb Ljava/lang/StringBuilder; L2 L6 1
    LOCALVARIABLE args [Ljava/lang/String; L0 L6 0
    MAXSTACK = 3
    MAXLOCALS = 3

According to the bytecode, it looks like the first StringBuilder appending a string template ends up having to instantiate a new StringBuilder to process the template and then add it to the sb StringBuilder. While the 2nd example, after sb2 was made, shows no instantiation for another StringBuilder as expected.

Is this intended behavior? Could the compiler be written to optimize the first example into the second? IntelliJ also detects this inspection in Java, but not Kotlin.


#2

To fix this, the compiler would have to special case StringBuilder. Basically if an appropriate append on a stringbuilder is invoked with an interpolated string it should shortcircuit it to produce “equivalent” code (of course the correct equivalence still evaluates sb.length before appending the surrounding string content so it would not quite be the same as your sb2).

One much simpler “special case” would be in those cases where the receiver of an interpolated string expects a CharSequence. It may be worthwhile to skip invoking toString on the StringBuilder in that case (but the semantics would be a bit different in the detail - where the receiver looks at the concrete type).

There are still many opportunities for the Kotlin compiler to be smarter in its bytecode generation (not that the Java compiler is smart, but JVM bytecode is closer to Java than to Kotlin), some of which may not be able to be optimized away by the JIT. This is just one of it.


#3

The compiler can optimize the StringBuilder creation for template strings.

In sb case the StringBuilder is created without any information about length, defer its creation after the evaluation of every string slice can ensure the right allocation

The code

"strings are cool ${sb.length}"

can be implemented as

val tmp1 = "strings are cool "
val tmp2 = String.valueOf(sb.length)
val tmp = StringBuilder(tmp1.length + tmp2.length) // always avoid reallocation
    .append(tmp1)
    .append(tmp2)
    .toString()

Note: Java 9 cover this use case in JEP-280


#4

Issue filed

https://youtrack.jetbrains.com/issue/KT-21147