文字列パフォーマンスのヒント

文字列パフォーマンスのヒント

**

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

StringBufferStringBuilderを説明するwrite-upがすでにあります。 したがって、ここでは、それらのパフォーマンスに関する追加情報のみを示します。 StringBuilder は、サイズ変更可能な配列と、配列で使用された最後のセルの位置を示すインデックスを使用します。 配列がいっぱいになると、サイズが2倍に拡張され、すべての文字が新しい配列にコピーされます。

サイズ変更があまり頻繁に発生しないことを考慮に入れると、we can consider each append() operation as O(1) constant timeです。 これを考慮に入れると、プロセス全体にO(n) の複雑さがあります。

StringBufferStringBuilder, の動的連結テストを変更して実行すると、次のようになります。

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.で呼び出します。batchSize10,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-consumingO(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で見つけることができます。

**