1. ホーム
  2. java

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

2023-09-23 05:27:11

質問

を調査しているときに 小さな議論 を使用しています。 "" + 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 マシン (最新の Mageia 4 64-bit, 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, ...) を使うと、さらに一桁ほど遅くなります。

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

なぜ StringBuilder.append(int) は、このJVMを使用すると非常に速く表示されるのでしょうか?を見ると StringBuilder クラスのソースコードを見てみると、特に興味深いことは何もありませんでした。 Integer#toString(int) . 興味深いことに、このメソッドに Integer.toString(int) (その stringBuilder2 マイクロベンチマーク) は高速化されていないように見えます。

このパフォーマンスの不一致は、テストハーネスの問題でしょうか?あるいは、私の OpenJDK JVM は、この特定のコード (アンチ) パターンに影響を与える最適化を含んでいますか?

EDIT。

よりわかりやすく比較するために、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 はより積極的な文字列最適化を行っていたのではないでしょうか?

編集 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で削除され、その代わりとなるものがなかったので、それが何か違いを生むかどうかは疑問です。残りのオプションは同じ設定であるように見えます。

EDIT 3:

のソースコードを横に並べて比較すると、このようになります。 AbstractStringBuilder , StringBuilderInteger クラスから src.zip ファイルから、特筆すべきことは何も見当たりません。多くの化粧品とドキュメントの変更を除いては。 Integer は符号なし整数をサポートするようになり StringBuilder との間でより多くのコードを共有するために若干リファクタリングされました。 StringBuffer . これらの変更はいずれも StringBuilder#append(int) で使用されるコードパスには影響を与えないようです。

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

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

私は、これは OpenJDK 7 ではるかに積極的な最適化パスの結果であるか、またはよりおそらく、特定の StringBuilder の演算を行うことができます。なぜこの最適化が私のJVM 8の実装で起きないのか、なぜ同じ最適化が Integer#toString(int) のために同じ最適化が実装されなかったのか、理由はわかりません。JREソースコードの関連部分に詳しい誰かがこれらの質問に答えなければならないと思います...。

どのように解決するのですか?

TL;DRです。 の副作用 append の副作用は、明らかに StringConcat の最適化を壊しています。

元の質問と更新における非常に良い分析です!

完全を期すために、以下はいくつかの欠けているステップです。

  • を通して見る -XX:+PrintInlining を7u55と8u5の両方で表示します。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 で 7u55 を実行すると、サブコールが再び出現し、パフォーマンスが 8u5 のレベルにまで低下します。

  • OK、ではなぜ8u5が同じ最適化を行わないのかを解明する必要があります。グレップ 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 をクリックして、そこでの最新の変更点を把握します。候補のひとつは、こうでしょう。

    changeset:   5493:90abdd727e64
    user:        iveresov
    date:        Wed Oct 16 11:13:15 2013 -0700
    summary:     8009303: Tiered: incorrect results in VM tests stringconcat...
    
    
  • OpenJDK メーリングリストのレビュースレッドを探してください (チェンジセット サマリーでググれば簡単に見つかります)。 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 の担当者と議論してください。オリジナルの修正の根拠は揺るぎないものですが、このような些細なケースでこの最適化を取り戻せるか、取り戻すべきかは興味深いところです。

  • ???

  • PROFITです。

そうそう、インクリメントを移動させるベンチマークの結果を投稿しておくと 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 チェーンの副作用による破損という仮説を立証するものです。