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

各メソッドは 1,000 万回呼び出されました。GC は各ブロック間で呼び出されました。バージョンを実行する順序が逆になり、同じ結果になりました。

私のテストでは、流暢なバージョンのコードは約 10% 遅いことが示されています (参考までに、コードのテストは一致しているが予測できない追加で公正であり、JVM のウォームアップなどに時間を割きました)。

流暢なコードは 1 行なので、これは驚きです。

非流暢なコードの方が速いのはなぜですか?

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

Java7アップデート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

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を実行した場合、時間の経過とともに明らかなバイアスはありません。

于 2012-12-31T11:51:05.323 に答える
3

まず、大規模なテスト (つまり、8 回の呼び出しではなく 10000 回) でベンチマークを繰り返し、ベンチマークを何度も実行し、全体を複​​数回実行して、結果が一貫しているかどうかを確認してください。

ソース コードの行数は、結果の速度とは無関係です。流暢な呼び出しには処理が必要な戻り値がありますが、非流暢な呼び出しは、戻り値を無視して、書き込まれることのない変数にアクセスするだけです。これは違いの可能な説明かもしれませんが、それほど大きくあるべきではないと思います.

于 2012-12-31T11:16:13.070 に答える
2

それはすべてJVMの最適化に依存しており、その動作を予測するのは困難です。オフ(-Xint)にすると、v.1の方が高速であることがわかります。1,000,000回の呼び出しがある私のPCでは、v.1は1466ミリ秒、v.21544ミリ秒を与えます。最適化を「オン」にすると、実際の違いはわかりません。とにかく、v.1のバイトコードは見栄えが良いです(私はEclipse用のA.Loskutovのバイトコードアウトラインプラグインを使用しています)

にとって

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 に答える
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 に答える