web-dev-qa-db-ja.com

StringBuilder#append(int)がJava 7よりもJava 8より速いのはなぜですか?

小さな議論 w.r.t. _"" + n_および Integer.toString(int) を使用して、整数プリミティブを文字列に変換し、これを書きました [〜#〜] jmh [〜#〜] マイクロベンチマーク:

_@Fork(1)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
public class IntStr {
    protected int counter;


    @GenerateMicroBenchmark
    public String integerToString() {
        return Integer.toString(this.counter++);
    }

    @GenerateMicroBenchmark
    public String stringBuilder0() {
        return new StringBuilder().append(this.counter++).toString();
    }

    @GenerateMicroBenchmark
    public String stringBuilder1() {
        return new StringBuilder().append("").append(this.counter++).toString();
    }

    @GenerateMicroBenchmark
    public String stringBuilder2() {
        return new StringBuilder().append("").append(Integer.toString(this.counter++)).toString();
    }

    @GenerateMicroBenchmark
    public String stringFormat() {
        return String.format("%d", this.counter++);
    }

    @Setup(Level.Iteration)
    public void prepareIteration() {
        this.counter = 0;
    }
}
_

Linuxマシンに存在するVM(最新のMageia 4 64ビット、Intel i7-3770 CPU、32GB RAM)の両方をJava VM)でデフォルトのJMHオプションで実行しました。最初のJVMは、Oracle JDK 8u5 64ビットで提供されたものでした。

_Java version "1.8.0_05"
Java(TM) SE Runtime Environment (build 1.8.0_05-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.5-b02, mixed mode)
_

このJVMを使用すると、私が期待したことはほとんど得られました。

_Benchmark                    Mode   Samples         Mean   Mean error    Units
b.IntStr.integerToString    thrpt        20    32317.048      698.703   ops/ms
b.IntStr.stringBuilder0     thrpt        20    28129.499      421.520   ops/ms
b.IntStr.stringBuilder1     thrpt        20    28106.692     1117.958   ops/ms
b.IntStr.stringBuilder2     thrpt        20    20066.939     1052.937   ops/ms
b.IntStr.stringFormat       thrpt        20     2346.452       37.422   ops/ms
_

つまりStringBuilderクラスを使用すると、StringBuilderオブジェクトを作成して空の文字列を追加するオーバーヘッドが増えるため、速度が低下します。 String.format(String, ...)の使用は、1桁ほど遅くなります。

一方、ディストリビューションが提供するコンパイラは、OpenJDK 1.7に基づいています。

_Java version "1.7.0_55"
OpenJDK Runtime Environment (mageia-2.4.7.1.mga4-x86_64 u55-b13)
OpenJDK 64-Bit Server VM (build 24.51-b03, mixed mode)
_

ここでの結果は興味深い

_Benchmark                    Mode   Samples         Mean   Mean error    Units
b.IntStr.integerToString    thrpt        20    31249.306      881.125   ops/ms
b.IntStr.stringBuilder0     thrpt        20    39486.857      663.766   ops/ms
b.IntStr.stringBuilder1     thrpt        20    41072.058      484.353   ops/ms
b.IntStr.stringBuilder2     thrpt        20    20513.913      466.130   ops/ms
b.IntStr.stringFormat       thrpt        20     2068.471       44.964   ops/ms
_

このJVMでStringBuilder.append(int)が非常に速く表示されるのはなぜですか? StringBuilderクラスのソースコードを見ると、特に興味深いものは何もありませんでした。問題のメソッドはInteger#toString(int)とほぼ同じです。興味深いことに、Integer.toString(int)(_stringBuilder2_ microbenchmark)の結果を追加しても、高速ではないようです。

このパフォーマンスの不一致は、テストハーネスの問題ですか?または、私のOpenJDK JVMには、この特定のコード(アンチ)パターンに影響する最適化が含まれていますか?

編集:

より簡単な比較のために、Oracle JDK 1.7u55をインストールしました。

_Java version "1.7.0_55"
Java(TM) SE Runtime Environment (build 1.7.0_55-b13)
Java HotSpot(TM) 64-Bit Server VM (build 24.55-b03, mixed mode)
_

結果はOpenJDKの結果と同様です。

_Benchmark                    Mode   Samples         Mean   Mean error    Units
b.IntStr.integerToString    thrpt        20    32502.493      501.928   ops/ms
b.IntStr.stringBuilder0     thrpt        20    39592.174      428.967   ops/ms
b.IntStr.stringBuilder1     thrpt        20    40978.633      544.236   ops/ms
_

これはより一般的なJava 7 vs Java 8の問題。おそらくJava 7にはより積極的な文字列最適化があった?

EDIT 2

完全を期すために、これら両方のJVMの文字列関連のVMオプションを以下に示します。

Oracle JDK 8u5の場合:

_$ /usr/Java/default/bin/Java -XX:+PrintFlagsFinal 2>/dev/null | grep String
     bool OptimizeStringConcat                      = true            {C2 product}
     intx PerfMaxStringConstLength                  = 1024            {product}
     bool PrintStringTableStatistics                = false           {product}
    uintx StringTableSize                           = 60013           {product}
_

OpenJDK 1.7の場合:

_$ Java -XX:+PrintFlagsFinal 2>/dev/null | grep String
     bool OptimizeStringConcat                      = true            {C2 product}        
     intx PerfMaxStringConstLength                  = 1024            {product}           
     bool PrintStringTableStatistics                = false           {product}           
    uintx StringTableSize                           = 60013           {product}           
     bool UseStringCache                            = false           {product}   
_

UseStringCacheオプションはJava 8で置き換えられずに削除されたため、違いはありません。残りのオプションは同じ設定になっているようです。

編集3:

の_src.Zip_ファイルからのAbstractStringBuilderStringBuilder、およびIntegerクラスのソースコードを並べて比較すると、何の不備もありません。外観とドキュメントの多くの変更とは別に、Integerは符号なし整数をサポートし、StringBuilderStringBufferとより多くのコードを共有するようにわずかにリファクタリングされました。これらの変更は、StringBuilder#append(int)が使用するコードパスに影響を与えるようには見えませんが、何かを見落としているかもしれません。

IntStr#integerToString()IntStr#stringBuilder0()に対して生成されたアセンブリコードの比較は、はるかに興味深いものです。 IntStr#integerToString()用に生成されたコードの基本的なレイアウトは、両方のJVMで類似していましたが、Oracle JDK 8u5はより攻撃的なw.r.tのようでした。 Integer#toString(int)コード内のいくつかの呼び出しをインライン化します。最小限のアセンブリの経験がある人でも、Javaソースコードと明確な対応がありました。

ただし、IntStr#stringBuilder0()のアセンブリコードは根本的に異なっていました。 Oracle JDK 8u5によって生成されたコードは、再びJavaソースコードに直接関連していました。同じレイアウトを簡単に認識できました。反対に、OpenJDK 7によって生成されたコードはほとんど認識できませんでした。訓練されていない目(私のような)。StringBuilderコンストラクターでの配列の作成と同様に、new StringBuilder()呼び出しは削除されたようです。 JDK 8で行ったようにソースコードに追加します。

これは、OpenJDK 7でのより積極的な最適化パスの結果であるか、特定のStringBuilder操作に手書きの低レベルコードを挿入した結果であると考えられます。 JVM 8実装でこの最適化が行われない理由、またはJVM 7でInteger#toString(int)に同じ最適化が実装されなかった理由がわかりません。JREソースコードの関連部分に精通している人は、これらの質問に答えなさい...

76
thkala

TL; DR:appendの副作用により、StringConcatの最適化が明らかに中断されます。

元の質問と更新で非常に優れた分析!

完全を期すために、不足しているいくつかの手順を次に示します。

  • 7u55と8u5の両方について、-XX:+PrintInliningを参照してください。 7u55では、次のようなものが表示されます。

     @ 16   org.sample.IntStr::inlineSideEffect (25 bytes)   force inline by CompilerOracle
       @ 4   Java.lang.StringBuilder::<init> (7 bytes)   inline (hot)
       @ 18   Java.lang.StringBuilder::append (8 bytes)   already compiled into a big method
       @ 21   Java.lang.StringBuilder::toString (17 bytes)   inline (hot)
    

    ...そして8u5で:

     @ 16   org.sample.IntStr::inlineSideEffect (25 bytes)   force inline by CompilerOracle
       @ 4   Java.lang.StringBuilder::<init> (7 bytes)   inline (hot)
         @ 3   Java.lang.AbstractStringBuilder::<init> (12 bytes)   inline (hot)
           @ 1   Java.lang.Object::<init> (1 bytes)   inline (hot)
       @ 18   Java.lang.StringBuilder::append (8 bytes)   inline (hot)
         @ 2   Java.lang.AbstractStringBuilder::append (62 bytes)   already compiled into a big method
       @ 21   Java.lang.StringBuilder::toString (17 bytes)   inline (hot)
         @ 13   Java.lang.String::<init> (62 bytes)   inline (hot)
           @ 1   Java.lang.Object::<init> (1 bytes)   inline (hot)
           @ 55   Java.util.Arrays::copyOfRange (63 bytes)   inline (hot)
             @ 54   Java.lang.Math::min (11 bytes)   (intrinsic)
             @ 57   Java.lang.System::arraycopy (0 bytes)   (intrinsic)
    

    7u55バージョンの方が浅く、StringBuilderメソッドの後に何も呼び出されていないように見えるかもしれません。これは、文字列の最適化が有効になっていることを示しています。実際、-XX:-OptimizeStringConcatで7u55を実行すると、サブコールが再表示され、パフォーマンスが8u5レベルに低下します。

  • それでは、8u5が同じ最適化を行わない理由を理解する必要があります。 Grep http://hg.openjdk.Java.net/jdk9/jdk9/hotspot 「StringBuilder」でVMがStringConcatの最適化を処理します。これにより、 src/share/vm/opto/stringopts.cppに入る

  • hg log src/share/vm/opto/stringopts.cppで最新の変更を見つけます。候補の1つは次のとおりです。

    changeset:   5493:90abdd727e64
    user:        iveresov
    date:        Wed Oct 16 11:13:15 2013 -0700
    summary:     8009303: Tiered: incorrect results in VM tests stringconcat...
    
  • OpenJDKメーリングリストでレビュースレッドを探します(変更セットの概要をgoogleで検索するのに十分簡単): http://mail.openjdk.Java.net/pipermail/hotspot-compiler-dev/2013-October/012084.html

  • スポット "文字列連結最適化最適化はパターンを文字列の単一の割り当てに折り畳み、結果を直接形成します。最適化されたコードで発生する可能性のあるすべての解除は、このパターンを最初から再開します(StringBuffer割り当てから開始) これは、パターン全体に副作用がないことを意味します。 "Eureka?

  • 対照的なベンチマークを記述します。

    @Fork(5)
    @Warmup(iterations = 5)
    @Measurement(iterations = 5)
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.NANOSECONDS)
    @State(Scope.Benchmark)
    public class IntStr {
        private int counter;
    
        @GenerateMicroBenchmark
        public String inlineSideEffect() {
            return new StringBuilder().append(counter++).toString();
        }
    
        @GenerateMicroBenchmark
        public String spliceSideEffect() {
            int cnt = counter++;
            return new StringBuilder().append(cnt).toString();
        }
    }
    
  • JDK 7u55で測定し、インライン化/スプライスされた副作用に対して同じパフォーマンスを確認します。

    Benchmark                       Mode   Samples         Mean   Mean error    Units
    o.s.IntStr.inlineSideEffect     avgt        25       65.460        1.747    ns/op
    o.s.IntStr.spliceSideEffect     avgt        25       64.414        1.323    ns/op
    
  • JDK 8u5で測定し、インライン効果によるパフォーマンスの低下を確認します。

    Benchmark                       Mode   Samples         Mean   Mean error    Units
    o.s.IntStr.inlineSideEffect     avgt        25       84.953        2.274    ns/op
    o.s.IntStr.spliceSideEffect     avgt        25       65.386        1.194    ns/op
    
  • バグレポート( https://bugs.openjdk.Java.net/browse/JDK-8043677 )を送信して、VM guys。の理論的根拠元の修正は堅実ですが、このようないくつかのささいなケースでこの最適化を取り戻すことができる場合は、興味深いものです。

  • ???

  • 利益。

そして、ええ、StringBuilderチェーンから増分を移動するベンチマークの結果を投稿し、チェーン全体の前にそれを行う必要があります。また、平均時間とns/opに切り替えました。これはJDK 7u55です。

Benchmark                      Mode   Samples         Mean   Mean error    Units
o.s.IntStr.integerToString     avgt        25      153.805        1.093    ns/op
o.s.IntStr.stringBuilder0      avgt        25      128.284        6.797    ns/op
o.s.IntStr.stringBuilder1      avgt        25      131.524        3.116    ns/op
o.s.IntStr.stringBuilder2      avgt        25      254.384        9.204    ns/op
o.s.IntStr.stringFormat        avgt        25     2302.501      103.032    ns/op

そして、これは8u5です。

Benchmark                      Mode   Samples         Mean   Mean error    Units
o.s.IntStr.integerToString     avgt        25      153.032        3.295    ns/op
o.s.IntStr.stringBuilder0      avgt        25      127.796        1.158    ns/op
o.s.IntStr.stringBuilder1      avgt        25      131.585        1.137    ns/op
o.s.IntStr.stringBuilder2      avgt        25      250.980        2.773    ns/op
o.s.IntStr.stringFormat        avgt        25     2123.706       25.105    ns/op

stringFormatは実際には8u5で少し高速であり、他のすべてのテストは同じです。これは、元の質問の主要な犯人であるSBチェーンの副作用破損の仮説を固めます。

94

これは、バイトコードがJITによってマシンコードにコンパイルされるタイミングを制御するCompileThresholdフラグと関係があると思います。

Oracle JDKのドキュメントとしてのデフォルトのカウントは10,000です http://www.Oracle.com/technetwork/Java/javase/tech/vmoptions-jsp-140102.html

OpenJDKでは、このフラグに関する最新のドキュメントが見つかりませんでした。しかし、いくつかのメールスレッドは、はるかに低いしきい値を示唆しています。 http://mail.openjdk.Java.net/pipermail/hotspot-compiler-dev/2010-November/004239.html

また、-XX:+UseCompressedStrings-XX:+OptimizeStringConcatなどのOracle JDKフラグをオンまたはオフにしてみてください。ただし、OpenJDKでこれらのフラグがデフォルトでオンになっているかどうかはわかりません。誰か提案してください。

できることの1つは、最初にプログラムを何度も(たとえば30,000ループ)実行し、System.gc()を実行してからパフォーマンスを確認することです。同じ結果が得られると思います。

また、GC設定も同じであると思います。そうしないと、多くのオブジェクトを割り当てることになり、GCが実行時間の大部分になる可能性があります。

5
Alex Suo