小さな議論 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
_ファイルからのAbstractStringBuilder
、StringBuilder
、およびInteger
クラスのソースコードを並べて比較すると、何の不備もありません。外観とドキュメントの多くの変更とは別に、Integer
は符号なし整数をサポートし、StringBuilder
はStringBuffer
とより多くのコードを共有するようにわずかにリファクタリングされました。これらの変更は、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ソースコードの関連部分に精通している人は、これらの質問に答えなさい...
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チェーンの副作用破損の仮説を固めます。
これは、バイトコードが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が実行時間の大部分になる可能性があります。