なぜJava 7のStringBuilder#append(int)はJava 8より速いのですか?
質問
を調査しているときに
小さな議論
を使用しています。
"" + 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
,
StringBuilder
と
Integer
クラスから
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 チェーンの副作用による破損という仮説を立証するものです。
関連
-
-bash: java: コマンドが見つからない 解決方法
-
[解決済み] JavaでStringをintに変換するにはどうしたらいいですか?
-
[解決済み] B "の印刷が "#"の印刷より劇的に遅いのはなぜですか?
-
[解決済み] 要素ごとの加算は、結合ループよりも分離ループの方がはるかに高速なのはなぜですか?
-
[解決済み] <は<=より速いのか?
-
[解決済み] なぜJavaにはtransientフィールドがあるのですか?
-
[解決済み] Java の toString() における StringBuilder と文字列連結の比較
-
[解決済み] なぜJavaでは2 * (i * i)の方が2 * i * iより速いのですか?
-
[解決済み] なぜ[]はlist()よりも速いのですか?
-
[解決済み】Javaで(a != 0 && b != 0)よりも(a*b != 0)の方が速いのはなぜか?
最新
-
nginxです。[emerg] 0.0.0.0:80 への bind() に失敗しました (98: アドレスは既に使用中です)
-
htmlページでギリシャ文字を使うには
-
ピュアhtml+cssでの要素読み込み効果
-
純粋なhtml + cssで五輪を実現するサンプルコード
-
ナビゲーションバー・ドロップダウンメニューのHTML+CSSサンプルコード
-
タイピング効果を実現するピュアhtml+css
-
htmlの選択ボックスのプレースホルダー作成に関する質問
-
html css3 伸縮しない 画像表示効果
-
トップナビゲーションバーメニュー作成用HTML+CSS
-
html+css 実装 サイバーパンク風ボタン
おすすめ
-
実行中にEclipseがポップアップする A Java Exception has occurred
-
Eclipse の問題 アクセス制限。タイプ 'jfxrt' はAPI解決されていません。
-
Springの設定でxsdファイルのバージョン番号を設定しない方が良い理由
-
無効な文字定数
-
シェルコマンドやスクリプトのJavaコール
-
Java基礎編 - オブジェクト指向
-
Javaがリソースリークに遭遇した:'input'が閉じない 解決方法
-
git pull appears現在のブランチに対するトラッキング情報がありません。
-
Java JDKのダイナミックプロキシ(AOP)の使用と実装の原理分析
-
Spring Bootは、Tomcatの組み込みのmaxPostSizeの値を設定します。