文字列パフォーマンスのヒント
**
1. 前書き
このチュートリアルでは、we’re going to focus on the performance aspect of the Java String API。
Stringの作成、変換、および変更操作を掘り下げて、使用可能なオプションを分析し、それらの効率を比較します。
これから行う提案は、必ずしもすべてのアプリケーションに適しているとは限りません。 ただし、確かに、アプリケーションの実行時間が重要な場合に、パフォーマンスを向上させる方法を示します。
2. 新しい文字列の作成
ご存じのとおり、Javaでは、文字列は不変です。 したがって、Stringオブジェクトを構築または連結するたびに、Javaは新しいString –を作成します。これは、ループで実行すると特にコストがかかる可能性があります。
2.1. コンストラクターの使用
ほとんどの場合、we should avoid creating Strings using the constructor unless we know what are we doingです。
最初にnew String()コンストラクターを使用して、次に=演算子を使用して、ループ内にnewString objectを作成しましょう。
ベンチマークを作成するには、JMH(Java Microbenchmark Harness)ツールを使用します。
私たちの構成:
@BenchmarkMode(Mode.SingleShotTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Measurement(batchSize = 10000, iterations = 10)
@Warmup(batchSize = 10000, iterations = 10)
public class StringPerformance {
}
ここでは、メソッドを1回だけ実行するSingeShotTimeモードを使用しています。 ループ内のString操作のパフォーマンスを測定したいので、それに使用できる@Measurementアノテーションがあります。
知っておくべき重要なことは、そのbenchmarking loops directly in our tests may skew the results because of various optimizations applied by JVMです。
したがって、単一の操作のみを計算し、JMHにループ処理を任せます。 簡単に言うと、JMHはbatchSizeパラメータを使用して反復を実行します。
それでは、最初のマイクロベンチマークを追加しましょう。
@Benchmark
public String benchmarkStringConstructor() {
return new String("example");
}
@Benchmark
public String benchmarkStringLiteral() {
return "example";
}
最初のテストでは、反復ごとに新しいオブジェクトが作成されます。 2番目のテストでは、オブジェクトは1回だけ作成されます。 残りの反復では、同じオブジェクトがString’s定数プールから返されます。
ループ反復カウント= 1,000,000でテストを実行し、結果を確認してみましょう。
Benchmark Mode Cnt Score Error Units
benchmarkStringConstructor ss 10 16.089 ± 3.355 ms/op
benchmarkStringLiteral ss 10 9.523 ± 3.331 ms/op
Scoreの値から、違いが重要であることがはっきりとわかります。
2.2. +演算子
動的なString連結の例を見てみましょう。
@State(Scope.Thread)
public static class StringPerformanceHints {
String result = "";
String example = "example";
}
@Benchmark
public String benchmarkStringDynamicConcat() {
return result + example;
}
結果では、平均実行時間を確認します。 出力数値の形式はミリ秒に設定されます。
Benchmark 1000 10,000
benchmarkStringDynamicConcat 47.331 4370.411
それでは、結果を分析しましょう。 ご覧のとおり、1000アイテムをstate.resultに追加するには、47.331ミリ秒かかります。 その結果、反復回数を10倍に増やすと、実行時間は4370.441ミリ秒に増加します。
要約すると、実行時間は2次関数的に増加します。 したがって、n回の反復のループにおける動的連結の複雑さはO(n^2)です。
2.3. String.concat()
Stringsを連結するもう1つの方法は、concat()メソッドを使用することです。
@Benchmark
public String benchmarkStringConcat() {
return result.concat(example);
}
出力時間単位はミリ秒、反復カウントは100,000です。 結果表は次のようになります。
Benchmark Mode Cnt Score Error Units
benchmarkStringConcat ss 10 3403.146 ± 852.520 ms/op
2.4. String.format()
文字列を作成する別の方法は、String.format()メソッドを使用することです。 Under the hood, it uses regular expressions to parse the input.
JMHテストケースを書いてみましょう。
String formatString = "hello %s, nice to meet you";
@Benchmark
public String benchmarkStringFormat_s() {
return String.format(formatString, example);
}
その後、実行して結果を確認します。
Number of Iterations 10,000 100,000 1,000,000
benchmarkStringFormat_s 17.181 140.456 1636.279 ms/op
String.format()を含むコードはよりクリーンで読みやすいように見えますが、パフォーマンスの点ではここでは勝ちません。
2.5. StringBuilderおよびStringBuffer
StringBufferとStringBuilderを説明するwrite-upがすでにあります。 したがって、ここでは、それらのパフォーマンスに関する追加情報のみを示します。 StringBuilder は、サイズ変更可能な配列と、配列で使用された最後のセルの位置を示すインデックスを使用します。 配列がいっぱいになると、サイズが2倍に拡張され、すべての文字が新しい配列にコピーされます。
サイズ変更があまり頻繁に発生しないことを考慮に入れると、we can consider each append() operation as O(1) constant timeです。 これを考慮に入れると、プロセス全体にO(n) の複雑さがあります。
StringBufferとStringBuilder, の動的連結テストを変更して実行すると、次のようになります。
Benchmark Mode Cnt Score Error Units
benchmarkStringBuffer ss 10 1.409 ± 1.665 ms/op
benchmarkStringBuilder ss 10 1.200 ± 0.648 ms/op
スコアの差はそれほど大きくありませんが、that StringBuilder works fasterに気付くことができます。
幸い、単純なケースでは、1つのStringを別のStringと配置するためにStringBuilderは必要ありません。 時々、static concatenation with + can actually replace StringBuilder. Under the hood, the latest Java compilers will call the StringBuilder.append() to concatenate strings。
これは、パフォーマンスの大幅な向上を意味します。
3. ユーティリティ操作
3.1. StringUtils.replace()とString.replace()
興味深いことに、そのApache Commons version for replacing the String does way better than the String’s own replace() methodです。 この違いに対する答えは、その実装にあります。 String.replace()は、正規表現パターンを使用してString.と一致させます
対照的に、StringUtils.replace()はindexOf()を広く使用しており、これはより高速です。
さて、ベンチマークテストの時間です。
@Benchmark
public String benchmarkStringReplace() {
return longString.replace("average", " average !!!");
}
@Benchmark
public String benchmarkStringUtilsReplace() {
return StringUtils.replace(longString, "average", " average !!!");
}
batchSizeを100,000に設定すると、次の結果が表示されます。
Benchmark Mode Cnt Score Error Units
benchmarkStringReplace ss 10 6.233 ± 2.922 ms/op
benchmarkStringUtilsReplace ss 10 5.355 ± 2.497 ms/op
数値の差はそれほど大きくありませんが、StringUtils.replace()の方がスコアが高くなります。 もちろん、数値とそれらの間のギャップは、繰り返し回数、文字列の長さ、さらにはJDKバージョンなどのパラメーターによって異なる場合があります。
最新のJDK 9(テストはJDK 10で実行されています)バージョンでは、両方の実装の結果はほぼ同じです。 それでは、JDKバージョンを8にダウングレードして、もう一度テストしてみましょう。
Benchmark Mode Cnt Score Error Units
benchmarkStringReplace ss 10 48.061 ± 17.157 ms/op
benchmarkStringUtilsReplace ss 10 14.478 ± 5.752 ms/op
パフォーマンスの違いは今では非常に大きく、最初に説明した理論を裏付けています。
3.2. split()
始める前に、Javaで使用可能な文字列splitting methodsを確認すると便利です。
文字列を区切り文字で分割する必要がある場合、最初に頭に浮かぶ関数は通常String.split(regex)です。 ただし、正規表現の引数を受け入れるため、深刻なパフォーマンスの問題が発生します。 または、StringTokenizerクラスを使用して、文字列をトークンに分割することもできます。
もう1つのオプションは、GuavaのSplitterAPIです。 最後に、正規表現の機能が必要ない場合は、古き良きindexOf()を使用してアプリケーションのパフォーマンスを向上させることもできます。
次に、String.split()オプションのベンチマークテストを作成します。
String emptyString = " ";
@Benchmark
public String [] benchmarkStringSplit() {
return longString.split(emptyString);
}
Pattern.split():
@Benchmark
public String [] benchmarkStringSplitPattern() {
return spacePattern.split(longString, 0);
}
StringTokenizer:
List stringTokenizer = new ArrayList<>();
@Benchmark
public List benchmarkStringTokenizer() {
StringTokenizer st = new StringTokenizer(longString);
while (st.hasMoreTokens()) {
stringTokenizer.add(st.nextToken());
}
return stringTokenizer;
}
String.indexOf():
List stringSplit = new ArrayList<>();
@Benchmark
public List benchmarkStringIndexOf() {
int pos = 0, end;
while ((end = longString.indexOf(' ', pos)) >= 0) {
stringSplit.add(longString.substring(pos, end));
pos = end + 1;
}
return stringSplit;
}
グアバのSplitter:
@Benchmark
public List benchmarkGuavaSplitter() {
return Splitter.on(" ").trimResults()
.omitEmptyStrings()
.splitToList(longString);
}
最後に、batchSize = 100,000の結果を実行して比較します。
Benchmark Mode Cnt Score Error Units
benchmarkGuavaSplitter ss 10 4.008 ± 1.836 ms/op
benchmarkStringIndexOf ss 10 1.144 ± 0.322 ms/op
benchmarkStringSplit ss 10 1.983 ± 1.075 ms/op
benchmarkStringSplitPattern ss 10 14.891 ± 5.678 ms/op
benchmarkStringTokenizer ss 10 2.277 ± 0.448 ms/op
ご覧のとおり、パフォーマンスが最も悪いのはbenchmarkStringSplitPatternメソッドで、Patternクラスを使用します。 その結果、split()メソッドで正規表現クラスを使用すると、パフォーマンスが何度も低下する可能性があることがわかります。
同様に、we notice that the fastest results are providing examples with the use of indexOf() and split().
3.3. Stringへの変換
このセクションでは、文字列変換の実行時スコアを測定します。 具体的には、Integer.toString()の連結方法を調べます。
int sampleNumber = 100;
@Benchmark
public String benchmarkIntegerToString() {
return Integer.toString(sampleNumber);
}
String.valueOf():
@Benchmark
public String benchmarkStringValueOf() {
return String.valueOf(sampleNumber);
}
[some integer value] + “”:
@Benchmark
public String benchmarkStringConvertPlus() {
return sampleNumber + "";
}
String.format():
String formatDigit = "%d";
@Benchmark
public String benchmarkStringFormat_d() {
return String.format(formatDigit, sampleNumber);
}
テストを実行すると、batchSize = 10,000の出力が表示されます。
Benchmark Mode Cnt Score Error Units
benchmarkIntegerToString ss 10 0.953 ± 0.707 ms/op
benchmarkStringConvertPlus ss 10 1.464 ± 1.670 ms/op
benchmarkStringFormat_d ss 10 15.656 ± 8.896 ms/op
benchmarkStringValueOf ss 10 2.847 ± 11.153 ms/op
結果を分析した後、the test for Integer.toString() has the best score of 0.953 millisecondsであることがわかります。 対照的に、String.format(“%d”)を含む変換のパフォーマンスは最低です。
形式Stringの解析はコストのかかる操作であるため、これは論理的です。
3.4. 文字列の比較
Strings.を比較するさまざまな方法を評価してみましょう。反復回数は100,000です。
String.equals()操作のベンチマークテストは次のとおりです。
@Benchmark
public boolean benchmarkStringEquals() {
return longString.equals(example);
}
String.equalsIgnoreCase():
@Benchmark
public boolean benchmarkStringEqualsIgnoreCase() {
return longString.equalsIgnoreCase(example);
}
String.matches():
@Benchmark
public boolean benchmarkStringMatches() {
return longString.matches(example);
}
String.compareTo():
@Benchmark
public int benchmarkStringCompareTo() {
return longString.compareTo(example);
}
その後、テストを実行して結果を表示します。
Benchmark Mode Cnt Score Error Units
benchmarkStringCompareTo ss 10 2.561 ± 0.899 ms/op
benchmarkStringEquals ss 10 1.712 ± 0.839 ms/op
benchmarkStringEqualsIgnoreCase ss 10 2.081 ± 1.221 ms/op
benchmarkStringMatches ss 10 118.364 ± 43.203 ms/op
いつものように、数字はそれ自体を表しています。 matches()は、正規表現を使用して等式を比較するため、最も時間がかかります。
対照的に、the equals() and equalsIgnoreCase() are the best choices。
3.5. String.matches()とPrecompiled Pattern
それでは、String.matches()とMatcher.matches() patternsを別々に見てみましょう。 最初のものは、引数として正規表現を取り、実行する前にそれをコンパイルします。
したがって、String.matches()を呼び出すたびに、Pattern:がコンパイルされます。
@Benchmark
public boolean benchmarkStringMatches() {
return longString.matches(example);
}
2番目のメソッドは、Patternオブジェクトを再利用します。
Pattern longPattern = Pattern.compile(longString);
@Benchmark
public boolean benchmarkPrecompiledMatches() {
return longPattern.matcher(example).matches();
}
そして今、結果:
Benchmark Mode Cnt Score Error Units
benchmarkPrecompiledMatches ss 10 29.594 ± 12.784 ms/op
benchmarkStringMatches ss 10 106.821 ± 46.963 ms/op
ご覧のとおり、プリコンパイルされた正規表現との一致は約3倍速くなります。
3.6. 長さの確認
最後に、String.isEmpty()メソッドを比較してみましょう。
@Benchmark
public boolean benchmarkStringIsEmpty() {
return longString.isEmpty();
}
およびString.length()メソッド:
@Benchmark
public boolean benchmarkStringLengthZero() {
return emptyString.length() == 0;
}
まず、それらをlongString = “Hello example, I am a bit longer than other Strings in average” String.で呼び出します。batchSizeは10,000です。
Benchmark Mode Cnt Score Error Units
benchmarkStringIsEmpty ss 10 0.295 ± 0.277 ms/op
benchmarkStringLengthZero ss 10 0.472 ± 0.840 ms/op
その後、longString = “”の空の文字列を設定して、テストを再実行しましょう。
Benchmark Mode Cnt Score Error Units
benchmarkStringIsEmpty ss 10 0.245 ± 0.362 ms/op
benchmarkStringLengthZero ss 10 0.351 ± 0.473 ms/op
お気づきのとおり、どちらの場合もbenchmarkStringLengthZero()メソッドとbenchmarkStringIsEmpty() メソッドのスコアはほぼ同じです。 ただし、isEmpty() works faster than checking if the string’s length is zeroを呼び出します。
4. 文字列の重複排除
JDK 8以降、文字列重複排除機能を使用してメモリ消費を排除できます。 簡単に言えば、this tool is looking for the strings with the same or duplicate contents to store one copy of each distinct string value into the String poolです。
現在、Stringの重複を処理する方法は2つあります。
-
String.intern()を手動で使用する
-
文字列の重複排除を有効にする
各オプションを詳しく見ていきましょう。
4.1. String.intern()
先に進む前に、write-upでの手動インターンについて読むと便利です。 With String.intern() we can manually set the reference of the String object inside of the global String pool。
その後、JVMは必要なときに参照を返すことができます。 パフォーマンスの観点から、アプリケーションは定数プールからの文字列参照を再利用することで大きなメリットを得ることができます。
知っておくべき重要なことは、そのJVM String pool isn’t local for the thread. Each String that we add to the pool, is available to other threads as wellです。
ただし、重大な欠点もあります。
-
アプリケーションを適切に維持するには、プールサイズを増やすために-XX:StringTableSizeJVMパラメータを設定する必要がある場合があります。 JVMはプールサイズを拡張するために再起動が必要です
-
calling String.intern() manually is time-consuming。 O(n)の複雑さの線形時間アルゴリズムで成長します
-
さらに、frequent calls on long String objects may cause memory problems
いくつかの実証済みの数値を取得するために、ベンチマークテストを実行してみましょう。
@Benchmark
public String benchmarkStringIntern() {
return example.intern();
}
さらに、出力スコアはミリ秒単位です。
Benchmark 1000 10,000 100,000 1,000,000
benchmarkStringIntern 0.433 2.243 19.996 204.373
ここでの列ヘッダーは、1000から1,000,000までのさまざまなiterationsカウントを表します。 反復回数ごとに、テストパフォーマンススコアがあります。 気づいたように、反復回数に加えてスコアは劇的に増加します。
4.2. 重複排除を自動的に有効にする
まず、this option is a part of the G1 garbage collector.デフォルトでは、この機能は無効になっています。 そのため、次のコマンドで有効にする必要があります。
-XX:+UseG1GC -XX:+UseStringDeduplication
enabling this option doesn’t guarantee that String deduplication will happenであることに注意してください。 また、若いStrings.を処理しません。Strings, XX:StringDeduplicationAgeThreshold=3を処理する最小経過時間を管理するために、JVMオプションを使用できます。 ここで、3がデフォルトのパラメータです。
5. 概要
このチュートリアルでは、日常のコーディング生活で文字列をより効率的に使用するためのヒントをいくつか紹介します。
その結果、we can highlight some suggestions in order to boost our application performance:
-
頭に浮かぶwhen concatenating strings, the StringBuilder is the most convenient option。 ただし、文字列が小さい場合、+ 操作のパフォーマンスはほぼ同じです。 内部的には、JavaコンパイラはStringBuilder classを使用して文字列オブジェクトの数を減らすことができます
-
値を文字列に変換するには、[some type].toString()(たとえば、Integer.toString())はString.valueOf()よりも高速に動作します。 その違いは重要ではないため、String.valueOf()を自由に使用して、入力値の型に依存しないようにすることができます。
-
文字列の比較に関しては、これまでのところString.equals()に勝るものはありません。
-
Stringの重複排除は、大規模なマルチスレッドアプリケーションのパフォーマンスを向上させます。 ただし、String.intern()を使いすぎると、深刻なメモリリークが発生し、アプリケーションの速度が低下する可能性があります。
-
for splitting the strings we should use indexOf() to win in performance。 ただし、重要ではない場合には、String.split()関数が適している場合があります。
-
Pattern.match()を使用すると、文字列によってパフォーマンスが大幅に向上します
-
String.isEmpty()は文字列.length() ==0よりも高速です
また、keep in mind that the numbers we present here are just JMH benchmark results –したがって、これらの種類の最適化の影響を判断するには、常に独自のシステムとランタイムの範囲でテストする必要があります。
最後に、いつものように、ディスカッション中に使用されたコードはover on GitHubで見つけることができます。
**