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

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

調査中に議論の余地なし使用"" + nInteger.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 マシン (最新の 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の追加によるオーバーヘッドが追加されるため、速度が低下します。 を使用すると、さらに 1 桁ほど遅くなります。StringBuilderString.format(String, ...)

一方、ディストリビューションで提供されるコンパイラは 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

このパフォーマンスの不一致はテストハーネスの問題でしょうか? それとも、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 と Java 8 の問題であるようです。おそらく、Java 7 では文字列の最適化がより積極的に行われていたのでしょうか?

編集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:

のソース コードと、AbstractStringBuilderのファイルからのクラスを並べて比較しても、特筆すべき点は見つかりません。外観とドキュメントの大幅な変更を除けば、は符号なし整数をサポートするようになり、 とより多くのコードを共有するように若干リファクタリングされました。これらの変更はいずれも で使用されるコード パスには影響しないようですが、私が何か見逃している可能性があります。StringBuilderIntegersrc.zipIntegerStringBuilderStringBufferStringBuilder#append(int)

と 用に生成されたアセンブリ コードの比較はIntStr#integerToString()IntStr#stringBuilder0()はるかに興味深いものです。 用に生成されたコードの基本的なレイアウトは、IntStr#integerToString()両方の JVM で類似していますが、Oracle JDK 8u5 は、コード内の一部の呼び出しのインライン化に関してより積極的であるように見えますInteger#toString(int)。アセンブリの経験がほとんどない人でも、Java ソース コードとの明確な対応がわかりました。

しかし、のアセンブリ コードはIntStr#stringBuilder0()根本的に異なっていました。Oracle JDK 8u5 によって生成されたコードは、再び Java ソース コードに直接関連しており、同じレイアウトを簡単に認識できました。一方、OpenJDK 7 によって生成されたコードは、訓練されていない目 (私のような) にはほとんど認識できませんでした。呼び出しはnew StringBuilder()削除されたようで、コンストラクターでの配列の作成も削除されましたStringBuilder。さらに、逆アセンブラー プラグインは、JDK 8 ほど多くのソース コード参照を提供できませんでした。

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

ベストアンサー1

要約:副作用により、appendStringConcat の最適化が明らかに中断されます。

元の質問と更新の分析が非常に優れています。

完全を期すために、以下にいくつかの欠落した手順を示します。

  • 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これは、文字列の最適化が有効になっていることを示す良い兆候です。実際、 で 7u55 を実行すると-XX:-OptimizeStringConcat、サブコールが再び表示され、パフォーマンスは 8u5 レベルまで低下します。

  • さて、8u5が同じ最適化を行わない理由を理解する必要があります。GrepホットスポットVMがStringConcatの最適化をどこで処理するかを「StringBuilder」で調べます。これにより、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 の割り当てから開始して)再開します。つまり、パターン全体に副作用がない必要があります。「エウレカ?」

  • 対照的なベンチマークを書き出します。

    @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
    
  • バグレポートを送信してください(参考:) にアクセスして、VM 担当者とこの動作について話し合いました。元の修正の根拠は確固たるものですが、このような些細なケースでこの最適化を復元できるかどうかは興味深いところです。

  • ???

  • 利益。

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 チェーンの副作用による破損であるという仮説が確固たるものになります。

おすすめ記事