2

我写了一些测试代码,比较了使用8 次顺序的append()方法 作为一个流畅的接口与分别在 8 行中调用它的速度。StringBuilder

流利的:

StringBuilder s = new StringBuilder();
s.append(x)
.append(y)
.append(z); //etc

作为非流利的:

StringBuilder s = new StringBuilder();
s.append(x)
s.append(y)
s.append(z); //etc

每个方法被调用了 1000 万次。在每个块之间调用 GC。执行版本的顺序颠倒了相同的结果。

我的测试表明,流畅的代码版本慢了大约 10%(仅供参考,测试代码对于匹配但不可预测的附加是公平的,我给了 JVM 预热等时间)。

这是一个惊喜,因为流畅的代码是一行。

为什么非流利的代码会更快?

4

4 回答 4

6

我怀疑这是某些 Java 版本的功能。

如果我运行以下

public class Main {

    public static final int RUNS = 100000000;

    static final ThreadLocal<StringBuilder> STRING_BUILDER_THREAD_LOCAL = new ThreadLocal<StringBuilder>() {
        @Override
        protected StringBuilder initialValue() {
            return new StringBuilder();
        }
    };

    public static final StringBuilder myStringBuilder() {
        StringBuilder sb = STRING_BUILDER_THREAD_LOCAL.get();
        sb.setLength(0);
        return sb;
    }

    public static long testSeparate(String x, String y, String z) {
        long start = System.nanoTime();
        for (int i = 0; i < RUNS; i++) {
            StringBuilder s = myStringBuilder();
            s.append(x)
                    .append(y)
                    .append(z);
            dontOptimiseAway = s.toString();
        }
        long time = System.nanoTime() - start;
        return time;
    }

    public static long testChained(String x, String y, String z) {
        long start = System.nanoTime();
        for (int i = 0; i < RUNS; i++) {
            StringBuilder s = myStringBuilder();
            s.append(x);
            s.append(y);
            s.append(z);
            dontOptimiseAway = s.toString();
        }
        long time = System.nanoTime() - start;
        return time;
    }

    static String dontOptimiseAway = null;

    public static void main(String... args) {
        for (int i = 0; i < 10; i++) {
            long time1 = testSeparate("x", "y", "z");
            long time2 = testChained("x", "y", "z");
            System.out.printf("Average time separate %.1f ns, chained %.1f ns%n",
                    (double) time1 / RUNS, (double) time2 / RUNS);
        }
    }
}

使用 Java 7 更新 4

Average time separate 49.8 ns, chained 49.0 ns
Average time separate 50.7 ns, chained 49.3 ns
Average time separate 46.9 ns, chained 46.5 ns
Average time separate 46.6 ns, chained 46.4 ns
Average time separate 46.6 ns, chained 46.6 ns
Average time separate 47.6 ns, chained 47.3 ns
Average time separate 46.7 ns, chained 47.2 ns
Average time separate 46.7 ns, chained 47.0 ns
Average time separate 46.0 ns, chained 46.6 ns
Average time separate 46.7 ns, chained 46.3 ns

使用 Java 7 更新 10

Average time separate 50.4 ns, chained 50.0 ns
Average time separate 50.1 ns, chained 50.1 ns
Average time separate 45.9 ns, chained 46.5 ns
Average time separate 46.6 ns, chained 46.7 ns
Average time separate 46.3 ns, chained 46.4 ns
Average time separate 46.7 ns, chained 46.5 ns
Average time separate 46.2 ns, chained 46.4 ns
Average time separate 46.6 ns, chained 46.0 ns
Average time separate 46.4 ns, chained 46.2 ns
Average time separate 45.9 ns, chained 46.2 ns

最初可能看起来有轻微的偏差,但如果您的运行更新 10 随着时间的推移没有明显的偏差。

于 2012-12-31T11:51:05.323 回答
3

首先,请用更大的测试重复您的基准测试(即 10000 次而不是 8 次调用),在多次迭代中运行基准测试,并多次运行整个测试以查看结果是否一致。

源代码行数与结果的速度无关。流畅的调用有一个需要处理的返回值,而非流畅的调用只是访问一个从未写入的变量,忽略返回值。这可能是差异的可能解释,尽管我认为它不应该那么大。

于 2012-12-31T11:16:13.070 回答
2

我尝试了下面的测试并获得了两种方法的非常接近的结果(在某些运行中完全匹配) - 所有方法都在实际测试之前编译:

public class Test1 {

    public static void main(String[] arg) {
        //warm up
        for (int i = 0; i < 1_000; i++) {
            method1("" + i);
        }

        for (int i = 0; i < 1_000; i++) {
            method2("" + i);
        }

        //full gc + test method1
        System.gc();
        System.out.println("method1");
        long start = System.nanoTime();
        for (int i = 0; i < 1_000; i++) {
            method1("" + i);
        }
        long end = System.nanoTime();
        System.out.println("method1: " + (end - start) / 1_000_000);

        //full gc + test method2
        System.gc();
        System.out.println("method2");
        start = System.nanoTime();
        for (int i = 0; i < 1_000; i++) {
            method2("" + i);
        }
        end = System.nanoTime();
        System.out.println("method2: " + (end - start) / 1_000_000);
    }

    public static void method1(String seed) {
        StringBuilder sb = new StringBuilder(seed);
        for (int i = 0; i < 10000; i++) {
            sb.append(seed + i)
                    .append(seed + i)
                    .append(seed + i)
                    .append(seed + i)
                    .append(seed + i)
                    .append(seed + i);
        }
        if (sb.length() == 7) {
            System.out.println("ok"); //pretending we are doing something
        }
    }

    public static void method2(String seed) {
        StringBuilder sb = new StringBuilder(seed);
        for (int i = 0; i < 10000; i++) {
            sb.append(seed + i);
            sb.append(seed + i);
            sb.append(seed + i);
            sb.append(seed + i);
            sb.append(seed + i);
            sb.append(seed + i);
        }
        if (sb.length() == 7) {
            System.out.println("ok"); //pretending we are doing something
        }
    }
}
于 2012-12-31T11:29:39.583 回答
2

这一切都取决于 JVM 优化,其行为难以预测。如果您将其关闭(-Xint),那么您会看到 v.1 更快。在我有 1,000,000 次调用的 PC 上,v.1 提供 1466 毫秒,v.2 提供 1544 毫秒。随着优化“开启”,我看不到任何真正的区别。无论如何,v.1 的字节码看起来更好(我使用 A.Loskutov 的 Eclipse 字节码大纲插件)

为了

s.append(x)
.append(y)
.append(z);

它是

    ALOAD 1
    ALOAD 0
    GETFIELD test/Test1.x : Ljava/lang/String;
    INVOKEVIRTUAL java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    ALOAD 0
    GETFIELD test/Test1.y : Ljava/lang/String;
    INVOKEVIRTUAL java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    ALOAD 0
    GETFIELD test/Test1.z : Ljava/lang/String;
    INVOKEVIRTUAL java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;

并且对于

    s.append(x);
    s.append(y);
    s.append(z);

它是

    ALOAD 1
    ALOAD 0
    GETFIELD test/Test1.x : Ljava/lang/String;
    INVOKEVIRTUAL java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    POP
    ALOAD 1
    ALOAD 0
    GETFIELD test/Test1.y : Ljava/lang/String;
    INVOKEVIRTUAL java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    POP
    ALOAD 1
    ALOAD 0
    GETFIELD test/Test1.z : Ljava/lang/String;
    INVOKEVIRTUAL java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;
于 2012-12-31T12:17:44.287 回答