9

此示例中,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,但至少总体比率与预期一致。

4

5 回答 5

23

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

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

这些中的任何一个或两者都可能会增加StringBuilder您测试部分的测量时间。

请阅读此问题的答案以获取更多详细信息:如何在 Java 中编写正确的微基准测试?

于 2012-10-26T07:41:42.373 回答
5

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

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

我得出结论,这是一个测量工件。

字符串生成器:

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    }

抽象字符串生成器:

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 中同步的“缓慢”可以被认为是一个神话。我所做或阅读的所有测试都得出结论,通常没有理由浪费太多时间来避免同步。

于 2012-10-26T07:44:08.610 回答
2

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

这是我进行了 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 次执行无法预测确切的数字。

于 2012-10-26T07:44:42.107 回答
2

我建议

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

当您运行代码少于 10000 次时,它可能不会触发将代码编译为默认值-XX:CompileThreshold=10000。它这样做的部分原因是收集有关如何最好地优化代码的统计信息。但是,当循环触发编译时,它会为整个方法触发它,这可以使以后的循环看起来 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),因此操作变慢了

于 2012-10-26T08:16:28.700 回答
1

我稍微修改了您的代码并添加了预热循环。大多数时候我的观察结果是一致的,大多数时候 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
于 2012-10-26T08:08:26.327 回答