こんにちは。梶原です。
これは TECHSCORE Advent Calendar 2017 の9日目の記事です。
Java 9 がリリースされ、Java 8 の End of Life を本腰入れて考え始めなければいけない今日この頃。
いま更感が半端ないですが、文字列結合のベンチマーク レッツトライ (`・ω・´)です。
元ネタは我が TECHSCORE ブログが誇る人気シリーズ「あえて言うほどではない」の過去記事(2012年11月29日)です。
あえて言うほどではない 文字列結合 Java編
- プラス演算子
- String#concat()
- StringBuffer
- StringBuilder
上記の方法で結合する処理を 10万回ループ中に実施、Java 5, Java 6, Java 7 で計測しています。結果も非常に興味深いです。過去記事も是非ご覧ください!
今回は Java 7, Java 8, Java 9(執筆時点 9.0.1)での計測です。
ベンチマーク計測には jmh を利用しました。
OpenJDK: jmh
では、やってみましょう。
計測用コード
文字列結合を10万回ループ中に実施。計測はウォームアップ・イテレート回数ともに jmh のデフォルト値である 20 回で行いました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
package sample.jmh.test; import java.util.concurrent.TimeUnit; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.Mode; import org.openjdk.jmh.runner.Runner; import org.openjdk.jmh.runner.RunnerException; import org.openjdk.jmh.runner.options.Options; import org.openjdk.jmh.runner.options.OptionsBuilder; public class MyBenchmark1 { /** ループ回数:10万回 */ private static final int LOOP_COUNT = 100000; /** 結合する文字列 */ private static final String S1 = "sss"; @Benchmark public void test1() { // プラス演算子 String s = ""; for (int i = 0; i < LOOP_COUNT; i++) { s = s + S1; } } @Benchmark public void test2() { // String#concat() String s = ""; for (int i = 0; i < LOOP_COUNT; i++) { s = s.concat(S1); } } @Benchmark public void test3() { // StringBuffer StringBuffer buf = new StringBuffer(); for (int i = 0; i < LOOP_COUNT; i++) { buf.append(S1); } buf.toString(); } @Benchmark public void test4() { // StringBuilder StringBuilder bld = new StringBuilder(); for (int i = 0; i < LOOP_COUNT; i++) { bld.append(S1); } bld.toString(); } public static void main(String[] args) throws RunnerException { Options opt = new OptionsBuilder() .include(MyBenchmark1.class.getSimpleName()) .forks(1) .timeUnit(TimeUnit.MILLISECONDS) .mode(Mode.AverageTime) .build(); new Runner(opt).run(); } } |
実行結果ログのうち ベンチマークが記述された部分は以下のように出力されます。
1 2 3 4 5 |
Benchmark Mode Cnt Score Error Units MyBenchmark1.test1 avgt 20 5647.217 ± 138.807 ms/op MyBenchmark1.test2 avgt 20 4275.261 ± 148.679 ms/op MyBenchmark1.test3 avgt 20 0.971 ± 0.015 ms/op MyBenchmark1.test4 avgt 20 1.079 ± 0.026 ms/op |
test1(プラス演算子による結合)の実行には平均 5647.217 ミリ秒かかった、と読みます。
計測結果
計測用コードを 60 回実行した平均値(読みやすいように四捨五入しています)を、以下の表にまとめました。単位はミリ秒です。
ループ10万回 | Java 7 | Java 8 | Java 9 |
---|---|---|---|
プラス演算子 | 6152.228 | 5614.730 | 2009.843 |
String#concat() | 4407.053 | 4304.561 | 2193.133 |
StringBuffer | 1.523 | 1.088 | 1.345 |
StringBuilder | 1.112 | 1.083 | 0.415 |
何といっても目を引くのは、Java 9 の処理速度。
プラス演算子と String#concat() と StringBuilder、素晴らしいですよね。今回のような簡単な計測方法でも、さらっと Java 8 の 2倍速を叩き出しました。
Java 9 における文字列の処理効率向上「Compact Strings」と「Indify String Concatenation」
JEP 254: Compact Strings
JEP 280: Indify String Concatenation
まずは、プラス演算子の計測結果に注目します。Indify String Concatenation の影響を受けて爆速化したと思われます。
String (Java Platform SE 8 )
Java 8 までの常識は「文字列連結はStringBuilder (またはStringBuffer)クラスとそのappendメソッドを使って実装されています」でした。
String (Java SE 9 & JDK 9 )
ところが、Java 9 では上記の記述が API ドキュメントからまるっと削除され、代わりの記述に「文字列の連結と変換の詳細については、「Java™言語仕様」を参照してください」とあります。何と、StringBuilder を使わなくなったのです。Java 9 からの新常識。ぜひ覚えておきたいものです。
詳細はまた別の機会に書こうと思いますが、ざっくりと言うと「予め結合する文字列の長さの byte 配列を確保して文字列を詰めていき、String に変換する」が Java 9 プラス演算子の処理の中身です。
次に、プラス演算子と String#concat() と StringBuilder の処理効率向上の素、Compact Strings に簡単に触れておきます。
これまでの実装では、文字列を 2 バイトの char 配列として格納していました。
Java の中の人は考えました。「半角英数字や記号・数字など、文字コード Latin-1 を取り扱うことがほとんどなんだから 1 バイト 無駄にしている」と。「だったら、Latin-1 (1 バイト)で持とうよ」。
結果、文字列を Latin-1、1 バイトの byte 配列として格納するようなった= Compact Strings です。Latin-1 以外の文字、例えば日本語「あ」の場合ではこれまでどおり 2 バイトの UTF-16 として格納します。
たしかに、ぎゅっとコンパクト化されました。
でも、私達は漢字とひらがなデータを取り扱うことも多いのです...
残念な結果が予想されますが、やってみました。 ひらがな「あああ」文字列結合のベンチマーク レッツトライ (`・ω・´)です。
ループ10万回 | Java 7 | Java 8 | Java 9 |
---|---|---|---|
プラス演算子 | 6027.036 | 5511.940 | 4033.945 |
String#concat() | 4219.871 | 4245.241 | 4428.548 |
StringBuffer | 1.483 | 1.158 | 1.636 |
StringBuilder | 1.095 | 1.066 | 0.721 |
Latin-1 以外の文字列では Compact Strings の恩恵をおおいに受ける、ということはなさそうです。
Java 8 から Java 9 に変更したのに思ったほど処理速度が上がらない、というケースも出てきそうです。当たり前の話ですが、内部処理を理解してデータに沿った最適化が必要になるということでしょう。
それにしても StringBuffer。Java 8 よりも Java 9 の方が遅くなっています。いったいどうしたんでしょう...(´・ω・`)
また機会があれば調べてみたいと思います。
まとめ
- Java 9 では文字列結合は高速になる(ただし Latin-1 に限る !!)
- Java 9 ではプラス演算子の内部処理は StringBuilder (またはStringBuffer) ではない
- バージョンUP=処理の高速化ではなく、内部処理を理解した最適化が必要になる
Java 9 の新機能はまだまだあります。驚きと喜びに浸りつつ、少しずつでも読み解いて行きたいと思います。