【问题标题】:Why is a fluent implementation slower than a non-fluent one?为什么流畅的实现比非流畅的实现慢?
【发布时间】:2012-12-31 11:08:23
【问题描述】:

我写了一些测试代码,比较了使用StringBuilder 的append() 方法连续八次作为fluent interface 与在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 预热等时间)。

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

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

【问题讨论】:

  • 我会在生成的字节码中找到答案。
  • 这似乎不太可能......您之前的一个问题显然存在缺陷,您无法始终如一地重现结果。你测试了多少次?您是否尝试过颠倒 2 次测试的顺序(一次测试 A 然后 B,另一次测试 B 然后 A)?
  • append 的返回值this,必须在invokevirtual 方法调用之前从堆栈中取出popped。 JIT 编译器会在一定程度上缓解这种情况。
  • 我不会相信任何以简单方式完成的性能测试。参加 Kirk Pepperdine 的课程,学习如何进行真正的性能测试 :)。推荐阅读:javaperformancetuning.comhttp://kirk.blog-city.comkodewerk.com
  • @assylias 我已经添加了一些关于我测试它的方式的信息。

标签: java performance fluent-interface


【解决方案1】:

我怀疑这是某些 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 次,则随着时间的推移不会出现明显的偏差。

【讨论】:

  • 无论如何,轻微的偏差与问题中提到的 10% 相差甚远。还有很多 GC 正在进行,这本身就会在结果中引入噪音。
【解决方案2】:

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

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

【讨论】:

  • 另外,我们谈论的是 10% 的 fast 代码。我怀疑一个人可能会优化速度。但是仍然有人想知道 java SE final 类没有优化这样的代码。
  • 我对附加的 8 个项目的测试重复了 1000 万次
【解决方案3】:

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

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

【讨论】:

  • hmm 存在性能问题,超过 3t。更快的“StringBuilder 中的字符串浓度”,意味着替换 sb.append(seed + i);与 sb.append(seed).append(i);那么差异也更小
  • 我得到了与sb.append(seed).append(i) 相似的结果(在这两种情况下都比字符串连接快约 20%)。
【解决方案4】:

这一切都取决于 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;

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-02-04
    • 1970-01-01
    • 1970-01-01
    • 2017-01-20
    • 2019-12-17
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多