9

このでは、StringBuffer は実際には StringBuilder よりも高速ですが、反対の結果が予想されていました。

これは、JIT によって行われる最適化と関係がありますか? メソッドが同期されているにもかかわらず、StringBufferがStringBuilderよりも高速になる理由を知っている人はいますか?

コードとベンチマーク結果は次のとおりです。

public class StringOps {

    public static void main(String args[]) {

        long sConcatStart = System.nanoTime();
        String s = "";
        for(int i=0; i<1000; i++) {
            s += String.valueOf(i);
        }
        long sConcatEnd = System.nanoTime();

        long sBuffStart = System.nanoTime();
        StringBuffer buff = new StringBuffer();
        for(int i=0; i<1000; i++) {
            buff.append(i);
        }
        long sBuffEnd = System.nanoTime();

        long sBuilderStart = System.nanoTime();
        StringBuilder builder = new StringBuilder();
        for(int i=0; i<1000; i++) {
            builder.append(i);
        }
        long sBuilderEnd = System.nanoTime();

        System.out.println("Using + operator : " + (sConcatEnd-sConcatStart) + "ns");
        System.out.println("Using StringBuffer : " + (sBuffEnd-sBuffStart) + "ns");
        System.out.println("Using StringBuilder : " + (sBuilderEnd-sBuilderStart) + "ns");

        System.out.println("Diff '+'/Buff = " + (double)(sConcatEnd-sConcatStart)/(sBuffEnd-sBuffStart));
        System.out.println("Diff Buff/Builder = " + (double)(sBuffEnd-sBuffStart)/(sBuilderEnd-sBuilderStart));
    }
}


ベンチマーク結果:

Using + operator : 17199609ns
Using StringBuffer : 244054ns
Using StringBuilder : 4351242ns
Diff '+'/Buff = 70.47460398108615
Diff Buff/Builder = 0.056088353624091696


アップデート:

みんなありがとう。ウォームアップは確かに問題でした。ウォームアップ コードが追加されると、ベンチマークは次のように変更されました。

Using + operator : 8782460ns
Using StringBuffer : 343375ns
Using StringBuilder : 211171ns
Diff '+'/Buff = 25.576876592646524
Diff Buff/Builder = 1.6260518726529685


YMMV ですが、少なくとも全体的な比率は予想されるものと一致しています。

4

5 に答える 5

23

StringBuilderコードを確認しましたが、速度が遅いと思われる最も可能性の高い理由は、ベンチマークが JVM ウォームアップの影響を適切に考慮していないことです。この場合:

  • JVM の起動により、処理が必要なかなりの量のガベージが生成されます。
  • JIT コンパイルは、実行の途中で開始される場合があります。

StringBuilderこれらのいずれかまたは両方が、テストの一部で測定された時間に追加される可能性があります。

詳細については、この質問への回答をお読みください: How do I write a correct micro-benchmark in Java?

于 2012-10-26T07:41:42.373 に答える
5

どちらの場合もまったく同じコード fromjava.lang.AbstractStringBuilderが使用され、両方のインスタンスが同じ容量 (16) で作成されます。

唯一の違いはsynchronized、最初の呼び出しで を使用することです。

これは測定アーティファクトであると結論付けています。

StringBuilder :

228    public StringBuilder append(int i) {
229        super.append(i);
230        return this;
231    }

文字列バッファ:

345    public synchronized StringBuffer append(int i) {
346        super.append(i);
347        return this;
348    }

AbstractStringBuilder :

605     public AbstractStringBuilder append(int i) {
606         if (i == Integer.MIN_VALUE) {
607             append("-2147483648");
608             return this;
609         }
610         int appendedLength = (i < 0) ? Integer.stringSize(-i) + 1
611                                      : Integer.stringSize(i);
612         int spaceNeeded = count + appendedLength;
613         if (spaceNeeded > value.length)
614             expandCapacity(spaceNeeded);
615         Integer.getChars(i, spaceNeeded, value);
616         count = spaceNeeded;
617         return this;
618     }


110     void expandCapacity(int minimumCapacity) {
111         int newCapacity = (value.length + 1) * 2;
112         if (newCapacity < 0) {
113             newCapacity = Integer.MAX_VALUE;
114         } else if (minimumCapacity > newCapacity) {
115             newCapacity = minimumCapacity;
116         }
117         value = Arrays.copyOf(value, newCapacity);
118     }

(expandCapacity はオーバーライドされません)

このブログ投稿では、次のことについて詳しく説明しています。

  • マイクロベンチマークの難しさ
  • 測定したものを少し見ずにベンチマークの「結果」を投稿するべきではないという事実(ここでは一般的なスーパークラス)

最近のJDKでの同期の「遅さ」は神話と見なすことができることに注意してください。私が行った、または読んだすべてのテストでは、通常、同期を回避するために多くの時間を失う理由はないと結論付けています。

于 2012-10-26T07:44:08.610 に答える
2

そのコードを自分で実行すると、さまざまな結果が表示されます。StringBuffer の方が高速な場合もあれば、StringBuilder の方が高速な場合もあります。これの考えられる理由は、JVM warmup使用前にかかる時間でStringBufferありStringBuilder、@Stephen が述べているように、複数の実行で異なる可能性があります。

これは私が行った4回の実行の結果です: -

Using StringBuffer : 398445ns
Using StringBuilder : 272800ns

Using StringBuffer : 411155ns
Using StringBuilder : 281600ns

Using StringBuffer : 386711ns
Using StringBuilder : 662933ns

Using StringBuffer : 413600ns
Using StringBuilder : 270356ns

もちろん、4 つの実行だけに基づいて正確な数値を予測することはできません。

于 2012-10-26T07:44:42.107 に答える
2

私は提案します

  • 各ループを個別の方法に分割して、1 つの最適化が別の方法に影響を与えないようにします。
  • 最初の 10K 反復を無視する
  • テストを少なくとも 2 秒間実行します。
  • テストを複数回実行して、再現性を確保します。

10000 回未満のコードを実行すると、コードがデフォルトでコンパイルされるようにトリガーされない場合があります-XX:CompileThreshold=10000。これを行う理由の一部は、コードを最適化する方法に関する統計を収集することです。ただし、ループがコンパイルをトリガーすると、メソッド全体でトリガーされ、後のループが a) 開始前にコンパイルされるため、見栄えが良くなります。 b) 統計を収集せずにコンパイルされるため、悪化します。


次のコードを検討してください

public static void main(String... args) {
    int runs = 1000;
    for (int i = 0; i < runs; i++)
        String.valueOf(i);

    System.out.printf("%-10s%-10s%-10s%-9s%-9s%n", "+ oper", "SBuffer", "SBuilder", "+/Buff", "Buff/Builder");
    for (int t = 0; t < 5; t++) {
        long sConcatTime = timeStringConcat(runs);
        long sBuffTime = timeStringBuffer(runs);
        long sBuilderTime = timeStringBuilder(runs);

        System.out.printf("%,7dns %,7dns %,7dns ",
                sConcatTime / runs, sBuffTime / runs, sBuilderTime / runs);
        System.out.printf("%8.2f %8.2f%n",
                (double) sConcatTime / sBuffTime, (double) sBuffTime / sBuilderTime);
    }
}

public static double dontOptimiseAway = 0;

private static long timeStringConcat(int runs) {
    long sConcatStart = System.nanoTime();
    for (int j = 0; j < 100; j++) {
        String s = "";
        for (int i = 0; i < runs; i += 100) {
            s += String.valueOf(i);
        }
        dontOptimiseAway = Double.parseDouble(s);
    }
    return System.nanoTime() - sConcatStart;
}

private static long timeStringBuffer(int runs) {
    long sBuffStart = System.nanoTime();
    for (int j = 0; j < 100; j++) {
        StringBuffer buff = new StringBuffer();
        for (int i = 0; i < runs; i += 100)
            buff.append(i);
        dontOptimiseAway = Double.parseDouble(buff.toString());
    }
    return System.nanoTime() - sBuffStart;
}

private static long timeStringBuilder(int runs) {
    long sBuilderStart = System.nanoTime();
    for (int j = 0; j < 100; j++) {
        StringBuilder buff = new StringBuilder();
        for (int i = 0; i < runs; i += 100)
            buff.append(i);
        dontOptimiseAway = Double.parseDouble(buff.toString());
    }
    return System.nanoTime() - sBuilderStart;
}

ランで印刷 = 1000

+ oper    SBuffer   SBuilder  +/Buff   Buff/Builder
  6,848ns   3,169ns   3,287ns     2.16     0.96
  6,039ns   2,937ns   3,311ns     2.06     0.89
  6,025ns   3,315ns   2,276ns     1.82     1.46
  4,718ns   2,254ns   2,180ns     2.09     1.03
  5,183ns   2,319ns   2,186ns     2.23     1.06

ただし、実行数を増やすと = 10,000

+ oper    SBuffer   SBuilder  +/Buff   Buff/Builder
  3,791ns     400ns     357ns     9.46     1.12
  1,426ns     139ns     113ns    10.23     1.23
    323ns     141ns     117ns     2.29     1.20
    317ns     115ns      78ns     2.76     1.47
    317ns     127ns     103ns     2.49     1.23

実行数を 100,000 に増やすと、

+ oper    SBuffer   SBuilder  +/Buff   Buff/Builder
  3,946ns     195ns     128ns    20.23     1.52
  2,364ns     113ns      86ns    20.80     1.32
  2,189ns     142ns      95ns    15.34     1.49
  2,036ns     142ns      96ns    14.31     1.48
  2,566ns     114ns      88ns    22.46     1.29

注:+ループの時間計算量が O(N^2) であるため、操作が遅くなりました

于 2012-10-26T08:16:28.700 に答える
1

コードを少し修正し、ウォームアップ ループを追加しました。私の観察は、ほとんどの場合、StringBuilder の方が高速であるという一貫性があります。

仮想的にWindows 7で実行され、VMに2 GBのRAMが割り当てられているUbuntu12.04ボックスで実行しています。

public class StringOps {

public static void main(String args[]) {

    for(int j=0;j<10;j++){
        StringBuffer buff = new StringBuffer();
        for(int i=0; i<1000; i++) {
                buff.append(i);
        }
    buff = new StringBuffer();
    long sBuffStart = System.nanoTime();
    for(int i=0; i<10000; i++) {
                buff.append(i);
        }
    long sBuffEnd = System.nanoTime();


        StringBuilder builder = new StringBuilder();
        for(int i=0; i<1000; i++) {
                builder.append(i);
        }
    builder = new StringBuilder();
    long sBuilderStart = System.nanoTime();
    for(int i=0; i<10000; i++) {
                builder.append(i);
        }   
        long sBuilderEnd = System.nanoTime();

        if((sBuffEnd-sBuffStart)>(sBuilderEnd-sBuilderStart)) {
        System.out.println("String Builder is faster") ; 
    }
    else {
        System.out.println("String Buffer is faster") ;
    }
    }
}

}

結果は次のとおりです。

String Builder is faster
String Builder is faster
String Builder is faster
String Builder is faster
String Buffer is faster
String Builder is faster
String Builder is faster
String Builder is faster
String Builder is faster
String Builder is faster
于 2012-10-26T08:08:26.327 に答える