【问题标题】:Surrounding Scala Strings环绕 Scala 字符串
【发布时间】:2012-01-11 06:31:44
【问题描述】:

如果你在像“abc”+stringval+“abc”这样的单个语句中做某事,是一个不可变的字符串副本,还是两个(注意 abc 和 123 在编译时是常量)

奖金回合:使用像下面这样的 StringBuilder 会有更多或更少的开销吗?

  def surround(s:String, ss:String):String = {
    val surrounded = new StringBuilder(s.length() + 2*ss.length(), s)
    surrounded.insert(0,ss)
    surrounded.append(ss)
    surrounded.mkString
  }

或者有没有我不知道的更惯用的方式?

【问题讨论】:

    标签: string scala immutability stringbuilder string-concatenation


    【解决方案1】:

    它的开销比串联的开销小。但是您示例中的插入效率不高。以下内容更简洁,仅使用 appends 来提高效率。

    def surround(s:String, ss:String) =
      new StringBuilder(s.length() + 2*ss.length(), ss).append(s).append(ss).mkString
    

    【讨论】:

      【解决方案2】:

      我的第一个冲动是查看字节码并查看。所以,

      // test.scala
      object Comparison {
        def surround1(s: String, ss: String) = {
          val surrounded = new StringBuilder(s.length() + 2*ss.length(), s)
          surrounded.insert(0, ss)
          surrounded.append(ss)
          surrounded.mkString
        }
      
        def surround2(s: String, ss: String) = ss + s + ss 
      
        def surround3(s: String, ss: String) =  // Neil Essy
          new StringBuilder(s.length() + 2*ss.length(), ss).append(s).append(ss).mkString
      }
      

      然后:

      $ scalac -optimize test.scala
      $ javap -verbose Comparison$
      [... lots of output ...]
      

      大致而言,Neil Essy 和您的方法相同,但只有一个方法调用(以及一些堆栈噪音)。 surround2 被编译成类似

      val sb = new StringBuilder()
      sb.append(ss)
      sb.append(s)
      sb.append(ss)
      sb.toString
      

      我是 Scala(和 Java)的新手,所以我不知道查看字节码有多大用处 - 但它告诉你 this scalac 的作用这个代码。

      【讨论】:

      • 环绕 2 和环绕 3 的主要区别在于环绕 3 预先计算了支持 StringBuilder 所需的数组大小。这消除了扩展支持构建器的数组的可能性和成本,其细节隐藏在 StringBuilder 类中。
      【解决方案3】:

      在 Java 和现在的 Scala 中对此进行一些测试,使用 StringBuilder 的价值是值得怀疑的,除非您正在执行大量非常量字符串的附加。

      object AppendTimeTest {
          val tries = 500000
          def surround(s:String, ss:String) = {
              (1 to tries).foreach(_ => {
                  new StringBuilder(s.length() + 2*ss.length(), ss).append(s).append(ss).mkString
              })
              val start = System.currentTimeMillis()
              (1 to tries).foreach(_ => {
                  new StringBuilder(s.length() + 2*ss.length(), ss).append(s).append(ss).mkString
              })
              val stop = System.currentTimeMillis()
              val delta:Double = stop -start
              println("Total time: " + delta + ".\n Avg. time: " + (delta/tries))
          }
          def surroundStatic(s:String) = {
              (1 to tries).foreach(_ => {
                  "ABC" + s + "ABC"
              })
      
              val start = System.currentTimeMillis()
              (1 to tries).foreach(_ => {
                  "ABC" + s + "ABC"
              })
              val stop = System.currentTimeMillis()
      
              val delta:Double = stop -start
              println("Total time: " + delta + ".\n Avg. time: " + (delta/tries))
          }
      }
      

      在解释器中调用几次会产生:

      scala> AppendTimeTest.surroundStatic("foo")
      Total time: 241.0.
       Avg. time: 4.82E-4
      
      scala>  AppendTimeTest.surround("foo", "ABC")
      Total time: 222.0.
       Avg. time: 4.44E-4
      
      scala> AppendTimeTest.surroundStatic("foo")
      Total time: 231.0.
       Avg. time: 4.62E-4
      
      scala>  AppendTimeTest.surround("foo", "ABC")
      Total time: 247.0.
       Avg. time: 4.94E-4
      

      因此,除非您附加许多不同的非常量字符串,否则我相信您不会看到性能上有任何大的差异。此外,连接常量(即"ABC" + "foo" + "ABC")可能由编译器处理(至少在 Java 中是这种情况,但我相信它也适用于 Scala)

      【讨论】:

      • 您应该尝试使用超过 16 个字符的最终字符串总长度,因为零参数 StringBuilder 最初就是这个长度。
      • 我无耻地窃取了@Neil Essy 的附加代码:)。如果我用零参数替换 StringBuilder 并使用 append 添加字符串,我会得到类似的结果。还尝试稍微增加静态字符串,即“ABCDEFGHIJKLMNOPQRSTUVXYZ”
      • @Luigi 我意识到你的意思是尝试使用相同的代码,但字符串长度超过 16 个字符,而不是将 StringBuilder 更改为零 arg (因为这是在幕后发生的事情无论如何,环绕静态方法)......这样做实际上会产生一点速度差异,对于环绕静态方法来说,速度大约慢了约 1.6 倍。
      • 是的,这就是我的意思,因为在 surroundStatic 方法中,幕后 StringBuilder 必须在追加时重新调整大小。有趣的结果。
      【解决方案4】:

      Scala 在字符串操作方面非常接近 java。你的例子:

      val stringval = "bar"
      "abc" + stringval + "abc"
      

      实际上最终(以 java 风格)为:

      (new StringBuilder()).append("abc").append(stringval()).append("abc").toString()
      

      这和java的行为是一样的,字符串之间的+一般都翻译成StringBuilder的实例,这样效率更高。所以在这里,您要对 StringBuilder 进行三份副本,最后创建一个 String,根据字符串的长度,可能会重新分配三个。

      在你的例子中:

      def surround(s:String, ss:String):String = {
        val surrounded = new StringBuilder(s.length() + 2*ss.length(), s)
        surrounded.insert(0,ss)
        surrounded.append(ss)
        surrounded.mkString
      }
      

      您正在执行相同数量的副本 (3),但您只分配了一次。

      建议:如果字符串很小,请使用 +,它几乎没有什么区别。如果字符串相对较大,则使用相关长度初始化 StringBuilder,然后简单地追加。这对其他开发人员来说更清楚。

      与以往一样,对性能进行衡量,如果差异很小,则使用更简单的解决方案

      【讨论】:

        【解决方案5】:

        通常在 Java / Scala 中,源代码中的String 字面量是interned 以提高效率,这意味着它在您的代码中的所有副本都将引用同一个对象。所以“abc”只有一个“副本”。

        Java 和 Scala 没有 C++ 中的“常量”。使用val 初始化的变量是不可变的,但在一般情况下,从一个实例到下一个实例的 val 不一样(通过构造函数指定)。

        所以理论上编译器可以检查简单的情况,其中 val 将始终初始化相同的值并进行相应的优化,但它会增加额外的复杂性。你可以自己优化它,把它写成“abc123abc”。

        其他人已经解决了您的奖金问题。

        【讨论】:

          【解决方案6】:

          您也可以在String 上使用mkString

          def surround(s:String, ss:String) = s.mkString(ss, "", ss)
          

          它在内部也使用StringBuilder,但我喜欢它的阅读方式。 我什至会内联它。

          【讨论】:

            猜你喜欢
            • 2017-04-18
            • 1970-01-01
            • 2013-02-09
            • 2016-01-09
            • 1970-01-01
            • 2017-11-07
            • 1970-01-01
            • 1970-01-01
            • 2011-12-03
            相关资源
            最近更新 更多