Java 为什么流畅的实现比不流畅的慢?

Java 为什么流畅的实现比不流畅的慢?,java,performance,fluent-interface,Java,Performance,Fluent Interface,我编写了一些测试代码,比较了使用append()方法的速度(按顺序使用8次)与在8行中分别调用它的速度 流利地说: 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万次。在每个块之间

我编写了一些测试代码,比较了使用
append()
方法的速度(按顺序使用8次)与在8行中分别调用它的速度

流利地说:

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预热等时间)

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


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

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


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

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

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
        }
    }
}
公共类Test1{
公共静态void main(字符串[]arg){
//热身
对于(int i=0;i<1_000;i++){
方法1(“+i”);
}
对于(int i=0;i<1_000;i++){
方法2(“+i”);
}
//完整gc+测试方法1
gc();
System.out.println(“方法1”);
长启动=System.nanoTime();
对于(int i=0;i<1_000;i++){
方法1(“+i”);
}
long end=System.nanoTime();
System.out.println(“方法1:”+(结束-开始)/1_000_000);
//完整gc+测试方法2
gc();
System.out.println(“方法2”);
start=System.nanoTime();
对于(int i=0;i<1_000;i++){
方法2(“+i”);
}
end=System.nanoTime();
System.out.println(“方法2:”+(结束-开始)/1_000_000);
}
公共静态无效方法1(字符串种子){
StringBuilder sb=新的StringBuilder(种子);
对于(int i=0;i<10000;i++){
某人追加(种子+i)
.append(seed+i)
.append(seed+i)
.append(seed+i)
.append(seed+i)
.追加(种子+i);
}
如果(sb.length()==7){
System.out.println(“ok”);//假装我们在做什么
}
}
公共静态void方法2(字符串种子){
StringBuilder sb=新的StringBuilder(种子);
对于(int i=0;i<10000;i++){
某人追加(种子+i);
某人追加(种子+i);
某人追加(种子+i);
某人追加(种子+i);
某人追加(种子+i);
某人追加(种子+i);
}
如果(sb.length()==7){
System.out.println(“ok”);//假装我们在做什么
}
}
}

我怀疑这是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);
        }
    }
}
使用Java7更新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,则随着时间的推移不会出现明显的偏差。

这完全取决于JVM优化,其行为很难预测。如果你关掉它 (-Xint)然后您将看到v.1更快。在我的电脑上,第1版调用次数为1000000次,第1版为1466毫秒,第2版为1544毫秒。优化“开启”时,我看不到任何真正的区别。无论如何,v.1的字节码看起来更好(我使用A.Loskutov的Eclipse字节码大纲插件)

为了

是的

    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;
    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;
以及

    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;
    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;
    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;

我会在生成的字节码中找到答案。这似乎不太可能。。。您之前的一个问题显然有一个缺陷,您无法始终如一地重现结果。你测试了多少次?您是否尝试过反转两个测试的顺序(在一个过程中测试A然后测试B,在另一个过程中测试B然后测试A)?append的返回值
this
,必须在
invokevirtual
方法调用之前从堆栈中
pop
ped。JIT编译器将在某种程度上缓解这种情况。我不相信任何以过于简单的方式进行的性能测试。与Kirk Pepperdine一起学习课程,学习如何进行真正的性能测试:)。推荐阅读:@assylias我已经添加了一些关于我测试它的方法的信息。我们还讨论了10%的快速代码。我怀疑一个人是否能在很大程度上优化速度。然而,有人怀疑java SE final类并没有优化这样的代码。我对附加的8个项目的测试重复了1000万次。无论如何,轻微的偏差远远不是问题中提到的10%。还有很多GC正在进行,其本身会在结果中引入噪声。嗯,存在性能问题,超过3t。更快的“StringBuilder中的字符串集中”,意味着替换sb.append(seed+i);与某人一起附加(种子);然后diff就更小了,我用sb.append(seed).append(i)得到了类似的结果(在这两种情况下都比字符串连接快20%)。