Эффективная конкатенация строк в Scala

JVM оптимизирует конкатенацию строк с помощью + и заменяет ее на StringBuilder. Это должно быть одинаковым в Scala. Но что произойдет, если строки объединены с ++=?

var x = "x"
x ++= "y"
x ++= "z"

Насколько я знаю, эти методы рассматривают такие строки, как char seqences, поэтому даже если JVM создаст StringBuilder, это приведет ко многим вызовам методов, не так ли? Было бы лучше использовать StringBuilder вместо этого?

К какому типу относится строка, преобразованная неявно?

Ответы

Ответ 1

На самом деле неудобная истина StringOps обычно остается распределением:

scala> :pa
// Entering paste mode (ctrl-D to finish)

class Concat {
    var x = "x"
    x ++= "y"
    x ++= "z"
}

// Exiting paste mode, now interpreting.

defined class Concat

scala> :javap -prv Concat
Binary file Concat contains $line3.$read$$iw$$iw$Concat
  Size 1211 bytes
  MD5 checksum 1900522728cbb0ed0b1d3f8b962667ad
  Compiled from "<console>"
public class $line3.$read$$iw$$iw$Concat
  SourceFile: "<console>"
[snip]


  public $line3.$read$$iw$$iw$Concat();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=6, locals=1, args_size=1
         0: aload_0       
         1: invokespecial #19                 // Method java/lang/Object."<init>":()V
         4: aload_0       
         5: ldc           #20                 // String x
         7: putfield      #10                 // Field x:Ljava/lang/String;
        10: aload_0       
        11: new           #22                 // class scala/collection/immutable/StringOps
        14: dup           
        15: getstatic     #28                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
        18: aload_0       
        19: invokevirtual #30                 // Method x:()Ljava/lang/String;
        22: invokevirtual #34                 // Method scala/Predef$.augmentString:(Ljava/lang/String;)Ljava/lang/String;
        25: invokespecial #36                 // Method scala/collection/immutable/StringOps."<init>":(Ljava/lang/String;)V
        28: new           #22                 // class scala/collection/immutable/StringOps
        31: dup           
        32: getstatic     #28                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
        35: ldc           #38                 // String y
        37: invokevirtual #34                 // Method scala/Predef$.augmentString:(Ljava/lang/String;)Ljava/lang/String;
        40: invokespecial #36                 // Method scala/collection/immutable/StringOps."<init>":(Ljava/lang/String;)V
        43: getstatic     #28                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
        46: invokevirtual #42                 // Method scala/Predef$.StringCanBuildFrom:()Lscala/collection/generic/CanBuildFrom;
        49: invokevirtual #46                 // Method scala/collection/immutable/StringOps.$plus$plus:(Lscala/collection/GenTraversableOnce;Lscala/collection/generic/CanBuildFrom;)Ljava/lang/Object;
        52: checkcast     #48                 // class java/lang/String
        55: invokevirtual #50                 // Method x_$eq:(Ljava/lang/String;)V

Смотрите демонстрацию в этом ответе.

Изменить: Чтобы сказать больше, вы создаете String для каждого переназначения, так что вы не используете ни одного StringBuilder.

Однако оптимизация выполняется javac, а не компилятором JIT, поэтому для сравнения плодов одного и того же типа:

public class Strcat {
    public String strcat(String s) {
        String t = " hi ";
        String u = " by ";
        return s + t + u;    // OK
    }
    public String strcat2(String s) {
        String t = s + " hi ";
        String u = t + " by ";
        return u;            // bad
    }
}

тогда

$ scala
Welcome to Scala version 2.11.2 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_11).
Type in expressions to have them evaluated.
Type :help for more information.

scala> :se -Xprint:typer

scala> class K { def f(s: String, t: String, u: String) = s ++ t ++ u }
[[syntax trees at end of                     typer]] // <console>
def f(s: String, t: String, u: String): String = scala.this.Predef.augmentString(scala.this.Predef.augmentString(s).++[Char, String](scala.this.Predef.augmentString(t))(scala.this.Predef.StringCanBuildFrom)).++[Char, String](scala.this.Predef.augmentString(u))(scala.this.Predef.StringCanBuildFrom)

Плохо. Или, что еще хуже, развернуть объяснение Рекса:

  "abc" ++ "def"

  augmentString("abc").++[Char, String](augmentString("def"))(StringCanBuildFrom)

  collection.mutable.StringBuilder.newBuilder ++= new WrappedString(augmentString("def"))

  val b = collection.mutable.StringBuilder.newBuilder
  new WrappedString(augmentString("def")) foreach b.+=

Как пояснил Рекс, StringBuilder переопределяет ++=(String), но не Growable.++=(Traversable[Char]).

Если вы когда-нибудь задавались вопросом, что unaugmentString для:

    28: invokevirtual #40                 // Method scala/Predef$.augmentString:(Ljava/lang/String;)Ljava/lang/String;
    31: invokevirtual #43                 // Method scala/Predef$.unaugmentString:(Ljava/lang/String;)Ljava/lang/String;
    34: invokespecial #46                 // Method scala/collection/immutable/WrappedString."<init>":(Ljava/lang/String;)V

И просто чтобы показать, что вы, наконец, вызываете unadorned +=(Char), но после бокса и распаковки:

  public final scala.collection.mutable.StringBuilder apply(char);
    flags: ACC_PUBLIC, ACC_FINAL
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0       
         1: getfield      #19                 // Field b$1:Lscala/collection/mutable/StringBuilder;
         4: iload_1       
         5: invokevirtual #24                 // Method scala/collection/mutable/StringBuilder.$plus$eq:(C)Lscala/collection/mutable/StringBuilder;
         8: areturn       
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0       9     0  this   L$line10/$read$$iw$$iw$$anonfun$1;
               0       9     1     x   C
      LineNumberTable:
        line 9: 0

  public final java.lang.Object apply(java.lang.Object);
    flags: ACC_PUBLIC, ACC_FINAL, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0       
         1: aload_1       
         2: invokestatic  #35                 // Method scala/runtime/BoxesRunTime.unboxToChar:(Ljava/lang/Object;)C
         5: invokevirtual #37                 // Method apply:(C)Lscala/collection/mutable/StringBuilder;
         8: areturn       
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0       9     0  this   L$line10/$read$$iw$$iw$$anonfun$1;
               0       9     1    v1   Ljava/lang/Object;
      LineNumberTable:
        line 9: 0

Хороший смех вызывает кислород в кровоток.

Ответ 2

Существует огромное ОГРОМНОЕ разница во времени.

Если вы добавляете строки повторно с помощью +=, вы не оптимизируете стоимость O(n^2) для создания инкрементирующих более длинных строк. Поэтому для добавления одного или двух вы не увидите различия, но не масштабируются; к моменту добавления 100 (коротких) строк использование StringBuilder более чем в 20 раз быстрее. (Точные данные: 1.3 us против 27.1 us, чтобы добавить строковые представления чисел от 0 до 100, тайминги должны быть воспроизводимыми примерно до + 5% и, конечно, для моей машины.)

Использование ++= в var String намного хуже, потому что вы затем инструктируете Scala обрабатывать строку как коллекцию символов по символам, которая затем требует, чтобы всевозможные обертки Строка выглядит как коллекция (включая добавление в полях с символом по символу с использованием общей версии ++!). Теперь вы на 16 раз медленнее на 100 дополнений! (Точные данные: 428.8 us для ++= в строке var вместо += 26.7 us.)

Если вы пишете один оператор с кучей + es, тогда компилятор Scala будет использовать StringBuilder и в итоге получит эффективный результат (Data: 1.8 us на непостоянных строках, выведенных из массива).

Итак, если вы добавляете строки с чем-либо, кроме + в строке, и вы заботитесь об эффективности, используйте StringBuilder. Определенно не используйте ++=, чтобы добавить еще один String в var String; там просто нет причин для этого, и там большой штраф времени исполнения.

(Примечание - очень часто вам все равно, насколько эффективны ваши строковые дополнения! Не загромождайте свой код дополнительным StringBuilder, если у вас нет причин подозревать, что этот конкретный путь кода получает много имен.)