【问题标题】:Why is StringBuilder slower than StringBuffer?为什么 StringBuilder 比 StringBuffer 慢?
【发布时间】:2012-10-16 11:22:20
【问题描述】:

this example 中,StringBuffer 实际上比 StringBuilder 快,而我预计会得到相反的结果。

这与 JIT 进行的优化有关吗?有谁知道为什么 StringBuffer 会比 StringBuilder 快,即使它的方法是同步的?

这是代码和基准测试结果:

public class StringOps {

    public static void main(String args[]) {

        long sConcatStart = System.nanoTime();
        String s = "";
        for(int i=0; i<1000; i++) {
            s += String.valueOf(i);
        }
        long sConcatEnd = System.nanoTime();

        long sBuffStart = System.nanoTime();
        StringBuffer buff = new StringBuffer();
        for(int i=0; i<1000; i++) {
            buff.append(i);
        }
        long sBuffEnd = System.nanoTime();

        long sBuilderStart = System.nanoTime();
        StringBuilder builder = new StringBuilder();
        for(int i=0; i<1000; i++) {
            builder.append(i);
        }
        long sBuilderEnd = System.nanoTime();

        System.out.println("Using + operator : " + (sConcatEnd-sConcatStart) + "ns");
        System.out.println("Using StringBuffer : " + (sBuffEnd-sBuffStart) + "ns");
        System.out.println("Using StringBuilder : " + (sBuilderEnd-sBuilderStart) + "ns");

        System.out.println("Diff '+'/Buff = " + (double)(sConcatEnd-sConcatStart)/(sBuffEnd-sBuffStart));
        System.out.println("Diff Buff/Builder = " + (double)(sBuffEnd-sBuffStart)/(sBuilderEnd-sBuilderStart));
    }
}


基准测试结果:

Using + operator : 17199609ns
Using StringBuffer : 244054ns
Using StringBuilder : 4351242ns
Diff '+'/Buff = 70.47460398108615
Diff Buff/Builder = 0.056088353624091696


更新:

感谢大家。热身确实是个问题。添加一些预热代码后,基准更改为:

Using + operator : 8782460ns
Using StringBuffer : 343375ns
Using StringBuilder : 211171ns
Diff '+'/Buff = 25.576876592646524
Diff Buff/Builder = 1.6260518726529685


YMMV,但至少总体比率符合预期。

【问题讨论】:

  • docs.oracle.com/javase/6/docs/api/java/lang/StringBuffer.html 说:通常应该优先使用 StringBuilder 类而不是这个(StringBuffer)类,因为它支持所有相同的操作,但速度更快,因为它不执行同步。跨度>
  • @DanIliescu OP 可能知道这一点。他很惊讶结果是圆的,同步的StringBuffer 更快。
  • @Parag 请发布您运行的基准代码以测试速度以及确切的结果,以便能够查看发生了什么...
  • @ppeterka 编辑问题以包含代码

标签: java stringbuilder stringbuffer


【解决方案1】:

我查看了您的代码,StringBuilder似乎变慢的最可能原因是您的基准测试没有正确考虑 JVM 预热的影响。在这种情况下:

  • JVM 启动会产生大量需要处理的垃圾,并且
  • JIT 编译可能会在运行过程中启动。

这两种情况之一或两者都可能会增加测试中StringBuilder 部分的测量时间。

请阅读此问题的答案以获取更多详细信息:How do I write a correct micro-benchmark in Java?

【讨论】:

    【解决方案2】:

    在这两种情况下都使用来自java.lang.AbstractStringBuilder 的完全相同的代码,并且创建的两个实例具有相同的容量 (16)。

    唯一的区别是在初始调用时使用synchronized

    我断定这是一个测量工件。

    StringBuilder :

    228    public StringBuilder append(int i) {
    229        super.append(i);
    230        return this;
    231    }
    

    字符串缓冲区:

    345    public synchronized StringBuffer append(int i) {
    346        super.append(i);
    347        return this;
    348    }
    

    AbstractStringBuilder :

    605     public AbstractStringBuilder append(int i) {
    606         if (i == Integer.MIN_VALUE) {
    607             append("-2147483648");
    608             return this;
    609         }
    610         int appendedLength = (i < 0) ? Integer.stringSize(-i) + 1
    611                                      : Integer.stringSize(i);
    612         int spaceNeeded = count + appendedLength;
    613         if (spaceNeeded > value.length)
    614             expandCapacity(spaceNeeded);
    615         Integer.getChars(i, spaceNeeded, value);
    616         count = spaceNeeded;
    617         return this;
    618     }
    
    
    110     void expandCapacity(int minimumCapacity) {
    111         int newCapacity = (value.length + 1) * 2;
    112         if (newCapacity < 0) {
    113             newCapacity = Integer.MAX_VALUE;
    114         } else if (minimumCapacity > newCapacity) {
    115             newCapacity = minimumCapacity;
    116         }
    117         value = Arrays.copyOf(value, newCapacity);
    118     }
    

    (expandCapacity 未被覆盖)

    这篇博文详细介绍了:

    • 微基准测试的难点
    • 事实上,您不应该在不查看您测量的内容(这里是常见的超类)的情况下发布基准的“结果”

    请注意,在最近的 JDK 中同步的“缓慢”可以被认为是一个神话。我所做或阅读的所有测试都得出结论,通常没有理由浪费太多时间来避免同步。

    【讨论】:

    • 虽然同步的开销比以前低得多,但它们仍然存在,如果您设计一个可以隔离它们的基准,它们将是可测量的。还值得注意的是,由于缓存刷新,不必要的同步会导致额外的内存流量。除了获取和释放锁的开销之外。
    【解决方案3】:

    当您在自己身上运行该代码时,您会看到不同的结果。有时 StringBuffer 更快,有时 StringBuilder 更快。 造成这种情况的可能原因可能是JVM warmup 在使用@Stephen 所述的StringBufferStringBuilder 之前所花费的时间,这可能因多次运行而异。

    这是我进行了 4 次运行的结果:-

    Using StringBuffer : 398445ns
    Using StringBuilder : 272800ns
    
    Using StringBuffer : 411155ns
    Using StringBuilder : 281600ns
    
    Using StringBuffer : 386711ns
    Using StringBuilder : 662933ns
    
    Using StringBuffer : 413600ns
    Using StringBuilder : 270356ns
    

    当然,仅基于 4 次执行无法预测确切的数字。

    【讨论】:

      【解决方案4】:

      我稍微修改了您的代码并添加了预热循环。 大多数时候我的观察结果是一致的,大多数时候 StringBuilder 更快。

      我在 Ubuntu12.04 机器上运行,它在 Windows 7 上虚拟运行,并为 VM 分配了 2 GB RAM。

      public class StringOps {
      
      public static void main(String args[]) {
      
          for(int j=0;j<10;j++){
              StringBuffer buff = new StringBuffer();
              for(int i=0; i<1000; i++) {
                      buff.append(i);
              }
          buff = new StringBuffer();
          long sBuffStart = System.nanoTime();
          for(int i=0; i<10000; i++) {
                      buff.append(i);
              }
          long sBuffEnd = System.nanoTime();
      
      
              StringBuilder builder = new StringBuilder();
              for(int i=0; i<1000; i++) {
                      builder.append(i);
              }
          builder = new StringBuilder();
          long sBuilderStart = System.nanoTime();
          for(int i=0; i<10000; i++) {
                      builder.append(i);
              }   
              long sBuilderEnd = System.nanoTime();
      
              if((sBuffEnd-sBuffStart)>(sBuilderEnd-sBuilderStart)) {
              System.out.println("String Builder is faster") ; 
          }
          else {
              System.out.println("String Buffer is faster") ;
          }
          }
      }
      

      }

      结果是:

      String Builder is faster
      String Builder is faster
      String Builder is faster
      String Builder is faster
      String Buffer is faster
      String Builder is faster
      String Builder is faster
      String Builder is faster
      String Builder is faster
      String Builder is faster
      

      【讨论】:

        【解决方案5】:

        我建议

        • 将每个循环分解为一个单独的方法,这样一个循环的优化就不会影响另一个循环。
        • 忽略前 10K 次迭代
        • 运行测试至少 2 秒。
        • 多次运行测试以确保其可重复性。

        当您运行代码少于 10000 次时,它可能不会触发将代码编译为默认 -XX:CompileThreshold=10000。它这样做的部分原因是收集有关如何最好地优化代码的统计信息。然而,当一个循环触发编译时,它会为 whole method 触发它,这可以使后面的循环看起来 a) 在开始之前编译时更好 b) 在不收集任何统计信息的情况下编译时更差.


        考虑下面的代码

        public static void main(String... args) {
            int runs = 1000;
            for (int i = 0; i < runs; i++)
                String.valueOf(i);
        
            System.out.printf("%-10s%-10s%-10s%-9s%-9s%n", "+ oper", "SBuffer", "SBuilder", "+/Buff", "Buff/Builder");
            for (int t = 0; t < 5; t++) {
                long sConcatTime = timeStringConcat(runs);
                long sBuffTime = timeStringBuffer(runs);
                long sBuilderTime = timeStringBuilder(runs);
        
                System.out.printf("%,7dns %,7dns %,7dns ",
                        sConcatTime / runs, sBuffTime / runs, sBuilderTime / runs);
                System.out.printf("%8.2f %8.2f%n",
                        (double) sConcatTime / sBuffTime, (double) sBuffTime / sBuilderTime);
            }
        }
        
        public static double dontOptimiseAway = 0;
        
        private static long timeStringConcat(int runs) {
            long sConcatStart = System.nanoTime();
            for (int j = 0; j < 100; j++) {
                String s = "";
                for (int i = 0; i < runs; i += 100) {
                    s += String.valueOf(i);
                }
                dontOptimiseAway = Double.parseDouble(s);
            }
            return System.nanoTime() - sConcatStart;
        }
        
        private static long timeStringBuffer(int runs) {
            long sBuffStart = System.nanoTime();
            for (int j = 0; j < 100; j++) {
                StringBuffer buff = new StringBuffer();
                for (int i = 0; i < runs; i += 100)
                    buff.append(i);
                dontOptimiseAway = Double.parseDouble(buff.toString());
            }
            return System.nanoTime() - sBuffStart;
        }
        
        private static long timeStringBuilder(int runs) {
            long sBuilderStart = System.nanoTime();
            for (int j = 0; j < 100; j++) {
                StringBuilder buff = new StringBuilder();
                for (int i = 0; i < runs; i += 100)
                    buff.append(i);
                dontOptimiseAway = Double.parseDouble(buff.toString());
            }
            return System.nanoTime() - sBuilderStart;
        }
        

        运行次数 = 1000

        + oper    SBuffer   SBuilder  +/Buff   Buff/Builder
          6,848ns   3,169ns   3,287ns     2.16     0.96
          6,039ns   2,937ns   3,311ns     2.06     0.89
          6,025ns   3,315ns   2,276ns     1.82     1.46
          4,718ns   2,254ns   2,180ns     2.09     1.03
          5,183ns   2,319ns   2,186ns     2.23     1.06
        

        但是,如果您增加运行次数 = 10,000

        + oper    SBuffer   SBuilder  +/Buff   Buff/Builder
          3,791ns     400ns     357ns     9.46     1.12
          1,426ns     139ns     113ns    10.23     1.23
            323ns     141ns     117ns     2.29     1.20
            317ns     115ns      78ns     2.76     1.47
            317ns     127ns     103ns     2.49     1.23
        

        如果我们将运行次数增加到 100,000,我会得到

        + oper    SBuffer   SBuilder  +/Buff   Buff/Builder
          3,946ns     195ns     128ns    20.23     1.52
          2,364ns     113ns      86ns    20.80     1.32
          2,189ns     142ns      95ns    15.34     1.49
          2,036ns     142ns      96ns    14.31     1.48
          2,566ns     114ns      88ns    22.46     1.29
        

        注意:由于循环的时间复杂度为 O(N^2),+ 操作已经变慢

        【讨论】:

        • 关于创建不同方法的要点。有什么特别的原因吗,你在方法中选择了双循环?
        • 我在循环中选择了一个循环,以防止 StringBuilder 的大小成为问题。假设最后一次测试中一个字符串中的数字数量约为 1000 而不是 100,000。即它试图使测试更加真实。
        猜你喜欢
        • 2018-02-27
        • 2023-04-04
        • 1970-01-01
        • 2011-12-26
        • 2012-06-22
        • 2012-10-09
        • 2014-08-26
        • 2011-02-24
        相关资源
        最近更新 更多