文字列パフォーマンスのヒント
1. 序章
このチュートリアルでは、 Java StringAPIのパフォーマンスの側面に焦点を当てます。
String の作成、変換、および変更操作を掘り下げて、使用可能なオプションを分析し、それらの効率を比較します。
これから行う提案は、必ずしもすべてのアプリケーションに適しているとは限りません。 ただし、確かに、アプリケーションの実行時間が重要な場合にパフォーマンスを向上させる方法を示します。
2. 新しい文字列の作成
ご存知のように、Javaでは文字列は不変です。 したがって、 String オブジェクトを構築または連結するたびに、Javaは新しい String – を作成します。これは、ループで実行すると特にコストがかかる可能性があります。
2.1。 コンストラクターの使用
ほとんどの場合、何をしているのかわからない限り、
最初にnewString()コンストラクターを使用して、次に = 演算子を使用して、ループ内にnewStringオブジェクトを作成しましょう。
ベンチマークを作成するには、 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アノテーションを使用できます。
ベンチマークループがテストで直接行われると、JVM によってさまざまな最適化が適用されるため、結果が歪む可能性があることを知っておくことが重要です。
したがって、単一の操作のみを計算し、JMHにループを処理させます。 簡単に言えば、JMHはbatchSizeパラメーターを使用して反復を実行します。
それでは、最初のマイクロベンチマークを追加しましょう。
@Benchmark
public String benchmarkStringConstructor() {
return new String("baeldung");
}
@Benchmark
public String benchmarkStringLiteral() {
return "baeldung";
}
最初のテストでは、反復ごとに新しいオブジェクトが作成されます。 2番目のテストでは、オブジェクトは1回だけ作成されます。 残りの反復では、同じオブジェクトがStringの定数プールから返されます。
ループ反復回数count= 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 baeldung = "baeldung";
}
@Benchmark
public String benchmarkStringDynamicConcat() {
return result + baeldung;
}
結果では、平均実行時間を確認したいと思います。 出力数値形式はミリ秒に設定されています。
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(baeldung);
}
出力時間の単位はミリ秒で、反復回数は100,000です。 結果テーブルは次のようになります。
Benchmark Mode Cnt Score Error Units
benchmarkStringConcat ss 10 3403.146 ± 852.520 ms/op
2.4. String.format()
文字列を作成する別の方法は、 String.format()メソッドを使用することです。 内部では、正規表現を使用して入力を解析します。
JMHテストケースを書いてみましょう。
String formatString = "hello %s, nice to meet you";
@Benchmark
public String benchmarkStringFormat_s() {
return String.format(formatString, baeldung);
}
その後、それを実行して結果を確認します。
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倍に拡張され、すべての文字が新しい配列にコピーされます。
サイズ変更があまり頻繁に発生しないことを考慮すると、各append()操作をO(1)定数時間と見なすことができます。 これを考慮に入れると、プロセス全体に 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
スコアの差はそれほど大きくありませんが、StringBuilderの動作が高速であることがわかります
幸い、単純なケースでは、1つのStringを別の
これは、パフォーマンスで大幅に勝つことを意味します。
3. ユーティリティ操作
3.1. StringUtils.replace()と String.replace()
興味深いことに、文字列を置き換えるための Apache Commonsバージョンは、文字列自体のreplace()メソッドよりもはるかに優れています。 この違いに対する答えは、それらの実装にあります。 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バージョンなどのパラメーターによって異なる場合があります。
最新のJDK9+(テストは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で利用可能な文字列分割メソッドを確認すると便利です。
区切り文字で文字列を分割する必要がある場合、最初に頭に浮かぶ関数は通常 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;
}
stringSplit.add(longString.substring(pos));
return stringSplit;
}
グアバのスプリッター:
@Benchmark
public List<String> 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()メソッドで正規表現クラスを使用すると、パフォーマンスが何度も低下する可能性があることがわかります。
同様に、最速の結果は、indexOf()とsplit()を使用した例を提供していることに気付きました。
3.3. 文字列に変換しています
このセクションでは、文字列変換の実行時スコアを測定します。 具体的には、 Integer.toString()連結メソッドを調べます。
int sampleNumber = 100;
@Benchmark
public String benchmarkIntegerToString() {
return Integer.toString(sampleNumber);
}
String.valueOf():
@Benchmark
public String benchmarkStringValueOf() {
return String.valueOf(sampleNumber);
}
[整数値]+“” :
@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
結果を分析した後、 Integer.toString()のテストのスコアが0.953ミリ秒であることがわかります。 対照的に、 String.format(“ %d”)を含む変換は、パフォーマンスが最も低くなります。
フォーマットStringの解析はコストのかかる操作であるため、これは論理的です。
3.4. 文字列の比較
文字列を比較するさまざまな方法を評価してみましょう。反復回数は100,000です。
String.equals()操作のベンチマークテストは次のとおりです。
@Benchmark
public boolean benchmarkStringEquals() {
return longString.equals(baeldung);
}
String.equalsIgnoreCase():
@Benchmark
public boolean benchmarkStringEqualsIgnoreCase() {
return longString.equalsIgnoreCase(baeldung);
}
String.matches():
@Benchmark
public boolean benchmarkStringMatches() {
return longString.matches(baeldung);
}
String.compareTo():
@Benchmark
public int benchmarkStringCompareTo() {
return longString.compareTo(baeldung);
}
その後、テストを実行して結果を表示します。
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()は、正規表現を使用して等式を比較するため、最も時間がかかります。
対照的に、 equals()とequalsIgnoreCase()が最良の選択です。
3.5. String.matches()とプリコンパイルされたパターン
それでは、 String.matches()パターンと Matcher.matches()パターンを別々に見てみましょう。 最初のものは、引数として正規表現を取り、実行する前にそれをコンパイルします。
したがって、 String.matches()を呼び出すたびに、 Pattern:がコンパイルされます。
@Benchmark
public boolean benchmarkStringMatches() {
return longString.matches(baeldung);
}
2番目のメソッドは、Patternオブジェクトを再利用します。
Pattern longPattern = Pattern.compile(longString);
@Benchmark
public boolean benchmarkPrecompiledMatches() {
return longPattern.matcher(baeldung).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 =“こんにちはbaeldung、平均して他のStringより少し長いです”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
お気づきのとおり、 branchmarkStringLengthZero()メソッドと branchmarkStringIsEmpty()メソッドは、どちらの場合もほぼ同じスコアです。 ただし、 isEmpty()の呼び出しは、文字列の長さがゼロであるかどうかを確認するよりも高速に機能します。
4. 文字列の重複排除
JDK 8以降、メモリ消費を排除するために文字列重複排除機能を使用できます。 簡単に言えば、このツールは、同じまたは重複した内容の文字列を探して、それぞれの個別の文字列値の1つのコピーを文字列プールに格納します。
現在、Stringの重複を処理する方法は2つあります。
- String.intern()を手動で使用する
- 文字列の重複排除を有効にする
各オプションを詳しく見てみましょう。
4.1. String.intern()
先に進む前に、記事で手動インターンについて読むと便利です。 String.intern()を使用すると、グローバル文字列プール内のStringオブジェクトの参照を手動で設定できます。
次に、JVMは必要に応じて参照を返すことができます。 パフォーマンスの観点から、私たちのアプリケーションは、定数プールからの文字列参照を再利用することで大きなメリットを得ることができます。
知っておくべき重要なこと
ただし、重大な欠点もあります。
- アプリケーションを適切に維持するには、 -XX:StringTableSizeJVMパラメーターを設定してプールサイズを増やす必要がある場合があります。 プールサイズを拡張するには、JVMを再起動する必要があります
- String.intern()を手動で呼び出すには時間がかかります。 O(n)の複雑さを持つ線形時間アルゴリズムで成長します
- さらに、長い文字列オブジェクトを頻繁に呼び出すと、メモリの問題が発生する可能性があります
いくつかの証明された数値を取得するために、ベンチマークテストを実行してみましょう。
@Benchmark
public String benchmarkStringIntern() {
return baeldung.intern();
}
さらに、出力スコアはミリ秒単位です。
Benchmark 1000 10,000 100,000 1,000,000
benchmarkStringIntern 0.433 2.243 19.996 204.373
ここでの列ヘッダーは、1000から1,000,000までのさまざまな反復カウントを表しています。 反復回数ごとに、テストパフォーマンススコアがあります。 お気づきのとおり、反復回数に加えてスコアが劇的に増加します。
4.2. 重複排除を自動的に有効にする
まず、このオプションはG1ガベージコレクターの一部です。デフォルトでは、この機能は無効になっています。 したがって、次のコマンドで有効にする必要があります。
-XX:+UseG1GC -XX:+UseStringDeduplication
このオプションを有効にしても、文字列の重複排除が発生することは保証されないことに注意してください。 また、若いStringsを処理しません。 Stringsの処理の最小経過時間を管理するために、XX:StringDeduplicationAgeThreshold = 3JVMオプションを使用できます。 ここで、3がデフォルトのパラメータです。
5. 概要
このチュートリアルでは、日常のコーディング生活で文字列をより効率的に使用するためのヒントを提供しようとしています。
その結果、アプリケーションのパフォーマンスを向上させるためにいくつかの提案を強調することができます:
- 文字列を連結する場合、StringBuilderが最も便利なオプションです。 ただし、文字列が小さい場合、+操作のパフォーマンスはほぼ同じです。 内部的には、Javaコンパイラは StringBuilder クラスを使用して、文字列オブジェクトの数を減らすことができます。
- 値を文字列に変換するには、 [some type] .toString()( Integer.toString()など)は
String.valueOf()[よりも高速に動作します。 X154X]。 その違いは重要ではないため、 String.valueOf()を自由に使用して、入力値の型に依存しないようにすることができます。 - 文字列の比較に関しては、これまでのところ String.equals()に勝るものはありません。
- String 重複排除により、大規模なマルチスレッドアプリケーションのパフォーマンスが向上します。 ただし、 String.intern()を使いすぎると、深刻なメモリリークが発生し、アプリケーションの速度が低下する可能性があります。
- 文字列を分割するには、indexOf()を使用してパフォーマンスを向上させる必要があります。 ただし、重要ではない場合には、 String.split()関数が適している場合があります。
- Pattern.match()を使用すると、文字列によってパフォーマンスが大幅に向上します
- String.isEmpty()はString .length()==0よりも高速です
また、ここに示す数値は単なるJMHベンチマーク結果であることに注意してください。したがって、これらの種類の最適化の影響を判断するには、常に独自のシステムとランタイムの範囲でテストする必要があります。
最後に、いつものように、ディスカッション中に使用されたコードは、GitHubのにあります。