Scala 中的高效字符串连接

Posted

技术标签:

【中文标题】Scala 中的高效字符串连接【英文标题】:Efficient string concatenation in Scala 【发布时间】:2014-09-02 16:36:39 【问题描述】:

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

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

据我所知,这种方法将字符串视为字符序列,因此即使 JVM 会创建一个 StringBuilder,它也会导致许多方法调用,对吧?改用 StringBuilder 会更好吗?

String 隐式转换成什么类型​​?

【问题讨论】:

你能不接受我的回答并选择 som-snytt 或 Rex Kerr 吗? 【参考方案1】:

所用时间存在巨大的巨大差异。

如果您使用+= 重复添加字符串,您不会优化O(n^2) 创建增量更长字符串的成本。因此,添加一两个您不会看到差异,但它不会扩展;当您添加 100 个(短)字符串时,使用 StringBuilder 的速度会快 20 倍以上。 (精确数据:1.3 us 与 27.1 us 添加数字 0 到 100 的字符串表示;计时应该可重现到大约 += 5%,当然适用于我的机器。)

var String 上使用++= 更糟糕,因为您随后指示Scala 将字符串视为逐字符的集合,然后需要各种包装器来制作字符串看起来像一个集合(包括使用通用版本的++)。现在你在 100 次加法上又慢了 16 倍! (精确数据:428.8 us for ++= on a var string 而不是 += 的 26.7 us。)

如果你用一堆+es 编写一个语句,那么 Scala 编译器将使用 StringBuilder 并最终得到一个高效的结果(数据:从数组中提取的非常量字符串为 1.8 us)。

因此,如果您在行中添加除+ 以外的任何字符串,并且您关心效率,请使用StringBuilder。绝对不要使用++= 将另一个String 添加到var String;没有任何理由这样做,而且运行时的损失很大。

(注意——通常你根本不关心你的字符串添加的效率有多高!除非你有理由怀疑这个特定的代码路径被称为很多。)

【讨论】:

我懒得去计时,但我不相信++= 是一个字符一个字符的; @om-nom-nom 关于“普通旧附加”是正确的,因为 TraversableLike 的 ++ 代表了构建器。也许并非总是如此;和 2.10 之前一样。 我最喜欢的 Odersky 提交消息(在 TraversableLike 上):Massive redesign so that: scala> "hi" == "hi".reverse.reverse。这听起来几乎像paulp一样。 (我应该说 TL 总是使用构建器的 ++=,并且 StringBuilder 在 2.10 中覆盖了它。) @som-snytt - StringBuilder 仅针对 String 覆盖 ++=,但这里它被键入为 Builder[Char, String],因此它使用通用路径 - 无论如何它都传递了一个参数它只知道是GenTraversableOnce[Char],所以它会经过整个未优化的路径。 @deamon - 我做了基准测试。请参阅我的帖子中的时间安排。 好的,谢谢。现在我得拿出我的特殊眼镜再看一遍。【参考方案2】:

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

在this answer查看更多演示。

编辑:多说,您在每次重新分配时都构建字符串,所以,不,您没有使用单个 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

开怀大笑确实会为血液注入氧气。

【讨论】:

以上是关于Scala 中的高效字符串连接的主要内容,如果未能解决你的问题,请参考以下文章

scala 字符串函数_Scala字符串连接,子字符串,长度函数

Json.obj Scala,字符串连接:编译错误

Scala:如何进行字符串连接以避免 GC 开销问题

高效的方法将键和值添加到scala中的Set Map

scala怎样创建redis集群连接池

Go——高效字符串连接