在Scala中高效地进行字符串拼接

17

JVM使用+优化字符串连接,并将其替换为StringBuilder。在Scala中应该也是这样的。但是,如果使用++=连接字符串会发生什么?

var x = "x"
x ++= "y"
x ++= "z"
据我所知,这种方法把字符串当作字符序列来处理,因此即使JVM创建了一个StringBuilder,也会导致许多方法调用,对吗?使用StringBuilder是否更好?
字符串会隐式转换为什么类型?

2
你能否取消接受我的答案,然后选择 som-snytt 或 Rex Kerr? - om-nom-nom
2个回答

16

时间消耗方面有巨大的差异。

如果你使用 += 反复连接字符串,你无法优化掉创建逐渐变长的字符串所带来的 O(n^2) 的代价。因此,当你需要连接数百个(较短的)字符串时,使用 StringBuilder 的速度会比使用 += 快 20 倍以上。 (精确数据:将数字 0 到 100 的字符串表示相加,+= 耗时 1.3 微秒,而 StringBuilder 耗时 27.1 微秒;计时误差约为 5%,当然是基于我的机器测试的。)

对于一个 var String 使用 ++= 更糟糕,因为这样你就让 Scala 将字符串视为一个字符集合进行处理,从而需要所有种类的包装器来使字符串看起来像一个集合(其中包括使用 ++ 的通用版本进行盒装的字符级加法!)。现在,在 100 次添加时,你的速度又慢了 16 倍!(精确数据:使用 ++= 对 var 字符串操作需要 428.8 微秒,而 += 只需要 26.7 微秒。)

如果你写一个有许多个 + 的单独语句,那么 Scala 编译器会使用 StringBuilder 并得出高效的结果(数据:对于从数组中提取的非常量字符串,耗时为 1.8 微秒)。

因此,如果你需要连接字符串但不是使用内联的 +,而且你关心效率,那么请使用 StringBuilder。绝对不要使用 ++= 将另一个 String 添加到 var String 中;这没有任何理由去这样做,而且会导致运行时间变长。

(注意 - 很多时候,您并不关心字符串的添加效率!除非您有理由怀疑该特定代码路径正在频繁调用,否则不要使用额外的StringBuilder使代码变得混乱。)


我太懒了,不想计时,但我不相信 ++= 是逐字符的;@om-nom-nom 关于“普通的追加”是正确的,因为 TraversableLike 的 ++ 委托给了 builder。也许以前不是这样的;就像在 2.10 之前一样。 - som-snytt
我的新最爱Odersky提交信息(关于TraversableLike):大规模重构,以便:scala>“hi”==“hi”.reverse.reverse。这听起来几乎像是paulp的样式。(我应该说TL总是使用builder的++=,而StringBuilder在2.10中被覆盖了。) - som-snytt
@som-snytt - StringBuilder 仅为 String 重写了 ++=,但在此处它被定义为 Builder[Char, String],因此使用通用路径--无论如何,它都会传递一个参数,该参数仅被认为是 GenTraversableOnce[Char],因此它将通过整个未优化的路径。 - Rex Kerr
@deamon - 我确实对它进行了基准测试。请查看我帖子中的时间记录。 - Rex Kerr
好的,谢谢。现在我得拿出我的特殊眼镜再看一遍。 - som-snytt
Rex,我原本以为在疯狂与重量比方面,设置层次结构已经够疯狂了。但我想有些事情不值得专门化。 - som-snytt

13
实际上,不方便的真相是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

请查看此答案以获取更多演示。

补充一下,每次重新分配时都会构建字符串,因此您并没有使用单个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)

很糟糕。甚至更糟糕的是,解释Rex的话可能会让情况更加复杂。

  "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.+=

作为 Rex 解释的一部分,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

为了说明你最终会调用未装饰的 +=(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

开怀大笑确实能够让氧气进入血液中。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接