調査中に議論の余地なし使用"" + 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 ビット、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 桁ほど遅くなります。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
この 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
のファイルからのクラスを並べて比較しても、特筆すべき点は見つかりません。外観とドキュメントの大幅な変更を除けば、は符号なし整数をサポートするようになり、 とより多くのコードを共有するように若干リファクタリングされました。これらの変更はいずれも で使用されるコード パスには影響しないようですが、私が何か見逃している可能性があります。StringBuilder
Integer
src.zip
Integer
StringBuilder
StringBuffer
StringBuilder#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
要約:副作用により、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
これは、文字列の最適化が有効になっていることを示す良い兆候です。実際、 で 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 チェーンの副作用による破損であるという仮説が確固たるものになります。