为什么流利的执行速度比不流利的执行速度慢?

时间:2012-12-31 11:08:24

标签: java performance fluent-interface

我编写了一些测试代码,将append() StringBuilder s = new StringBuilder(); s.append(x) .append(y) .append(z); //etc 方法的顺序与StringBuilder相比,将{8}分别按8行顺序进行比较。

流利:

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

不流利:

{{1}}

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

我的测试显示,代码的流畅版本慢了大约10%(fyi,测试代码是公平的,匹配但不可预测的附加,我给JVM热身时间等。)

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

为什么不流畅的代码会更快?

4 个答案:

答案 0 :(得分: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 update 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,则随着时间的推移没有明显的偏差。

答案 1 :(得分:3)

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

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

答案 2 :(得分: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
        }
    }
}

答案 3 :(得分:2)

这一切都取决于JVM优化,其行为很难预测。如果你关掉它 (-Xint)然后你会看到v.1更快。在我的PC上有1,000,000次调用v.1给出了1466 ms和v.2 1544 ms。优化'开'我看不出任何真正的差异。无论如何,v.1的字节码看起来更好(我使用A.Loskutov的Eclipse的Bytecode Outline插件)

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;