web-dev-qa-db-ja.com

パフォーマンスが重要な場合、JavaのString.format()を使用する必要がありますか?

ログ出力などのために、常に文字列を作成する必要があります。 JDKバージョンでは、StringBuffer(多数の追加、スレッドセーフ)およびStringBuilder(多数の追加、非スレッドセーフ)を使用する場合を学習しました。

String.format()の使用に関するアドバイスは何ですか?それは効率的ですか、それともパフォーマンスが重要なワンライナーの連結に固執する必要がありますか?

例えばoldい古いスタイル、

String s = "What do you get if you multiply " + varSix + " by " + varNine + "?";

整頓された新しいスタイル(String.format、おそらく遅い)、

String s = String.format("What do you get if you multiply %d by %d?", varSix, varNine);

注:私の特定のユースケースは、コード全体にわたる数百の「1行」ログ文字列です。ループを含まないため、StringBuilderは重すぎます。 String.format()に特に興味があります。

203
Air

私はテストする小さなクラスを作成しました。これは、2つのパフォーマンスが優れており、+はフォーマットよりも優先されます。 5〜6倍にします。自分で試してみてください

import Java.io.*;
import Java.util.Date;

public class StringTest{

    public static void main( String[] args ){
    int i = 0;
    long prev_time = System.currentTimeMillis();
    long time;

    for( i = 0; i< 100000; i++){
        String s = "Blah" + i + "Blah";
    }
    time = System.currentTimeMillis() - prev_time;

    System.out.println("Time after for loop " + time);

    prev_time = System.currentTimeMillis();
    for( i = 0; i<100000; i++){
        String s = String.format("Blah %d Blah", i);
    }
    time = System.currentTimeMillis() - prev_time;
    System.out.println("Time after for loop " + time);

    }
}

異なるNに対して上記を実行すると、両方とも直線的に動作しますが、String.formatは5〜30倍遅くなります。

その理由は、現在の実装ではString.formatが最初に入力を正規表現で解析してからパラメーターを入力するためです。一方、プラスとの連結は、javac(JITではなく)によって最適化され、StringBuilder.appendを直接使用します。

Runtime comparison

119
hhafez

hhafez コードを取得し、memory testを追加しました:

private static void test() {
    Runtime runtime = Runtime.getRuntime();
    long memory;
    ...
    memory = runtime.freeMemory();
    // for loop code
    memory = memory-runtime.freeMemory();

「+」演算子、String.format、およびStringBuilder(toString()を呼び出す)の各アプローチに対して個別に実行するため、使用されるメモリは他のアプローチの影響を受けません。さらに連結を追加して、文字列を「Blah」+ i +「Blah」+ i +「Blah」+ i +「Blah」として作成しました。

結果は次のとおりです(各5回の実行の平均)。
アプローチ時間(ミリ秒)割り当てられたメモリ(長い)
'+'演算子747 320,504
String.format 16484 373,312
StringBuilder 769 57,344

String '+'とStringBuilderは時間的には実質的に同一であることがわかりますが、StringBuilderはメモリ使用においてはるかに効率的です。これは、ガベージコレクターが '+'演算子の結果として発生する多くの文字列インスタンスを消去できないほど短い時間間隔で多くのログ呼び出し(または文字列を含む他のステートメント)がある場合に非常に重要です。

ところで、メッセージを作成する前に、ロギングlevelを確認することを忘れないでください。

結論:

  1. StringBuilderの使用を続けます。
  2. 時間がありすぎるか、人生が少なすぎる。
235
Itamar

ここに示されているすべてのベンチマークには、いくつかの flaws があるため、結果は信頼できません。

ベンチマークに JMH が使用されていないことに驚いたので、そうしました。

結果:

Benchmark             Mode  Cnt     Score     Error  Units
MyBenchmark.testOld  thrpt   20  9645.834 ± 238.165  ops/s  // using +
MyBenchmark.testNew  thrpt   20   429.898 ±  10.551  ops/s  // using String.format

単位は1秒あたりの操作であり、より優れています。 ベンチマークソースコード 。 OpenJDK IcedTea 2.5.4 Java仮想マシンが使用されました。

したがって、古いスタイル(+を使用)の方がはるかに高速です。

25

古いいスタイルは、次のようにJAVAC 1.6によって自動的にコンパイルされます。

StringBuilder sb = new StringBuilder("What do you get if you multiply ");
sb.append(varSix);
sb.append(" by ");
sb.append(varNine);
sb.append("?");
String s =  sb.toString();

したがって、これとStringBuilderの使用との間に違いはまったくありません。

String.formatは、新しいFormatterを作成し、入力フォーマット文字列を解析し、S​​tringBuilderを作成し、それにすべてを追加してtoString()を呼び出すため、はるかに重いです。

21
Raphaël

JavaのString.formatは次のように機能します。

  1. 書式文字列を解析し、書式チャンクのリストに分解します
  2. フォーマットチャンクを繰り返し、StringBuilderにレンダリングします。これは基本的に、新しい配列にコピーすることにより、必要に応じてサイズを変更する配列です。最終的な文字列を割り当てる大きさはまだわからないため、これが必要です。
  3. StringBuilder.toString()は、内部バッファを新しい文字列にコピーします

このデータの最終宛先がストリームの場合(たとえば、Webページのレンダリングまたはファイルへの書き込み)、フォーマットチャンクをストリームに直接アセンブルできます。

new PrintStream(outputStream, autoFlush, encoding).format("hello {0}", "world");

オプティマイザーは、フォーマット文字列の処理を最適化します。その場合、String.formatを手動でStringBuilderに展開するのと同等のパフォーマンス amortized が残っています。

12
Dustin Getz

上記の最初の答えを展開/修正するために、実際にはString.formatが役立つ翻訳ではありません。
String.formatが役立つのは、ローカリゼーション(l10n)の違いがある日付/時刻(または数値形式など)を印刷するときです(つまり、一部の国では04Feb2009が印刷され、他の国では印刷Feb042009)。
翻訳では、ResourceBundleとMessageFormatを使用して、適切な言語に適切なバンドルを使用できるように、外部化可能な文字列(エラーメッセージやその他のものなど)をプロパティバンドルに移動するだけです。

[。連結よりも.formatの呼び出しを見たい場合は、どうしてもそれを使用してください。
結局のところ、コードは書かれているよりもずっと多く読み取られます。

8
dw.mackie

あなたの例では、パフォーマンスのプロバルビーはそれほど違いはありませんが、考慮すべき他の問題があります。すなわち、メモリの断片化です。連結操作でさえ、一時的なものであっても、新しい文字列を作成します(GCに時間がかかり、より多くの作業が必要になります)。 String.format()は読みやすく、断片化が少なくなります。

また、特定の形式を多く使用している場合は、Formatter()クラスを直接使用できることを忘れないでください(String.format()はすべて、1回限りのFormatterインスタンスをインスタンス化します)。

また、他に知っておくべきこと:substring()の使用に注意してください。例えば:

String getSmallString() {
  String largeString = // load from file; say 2M in size
  return largeString.substring(100, 300);
}

その大きな文字列は、Java部分文字列が機能する方法であるため、まだメモリ内にあります。より良いバージョンは次のとおりです。

  return new String(largeString.substring(100, 300));

または

  return String.format("%s", largeString.substring(100, 300));

2番目の形式は、他の作業を同時に行う場合におそらくより便利です。

6
cletus

通常、String.Formatは比較的高速であり、グローバリゼーションをサポートしているため、String.Formatを使用する必要があります(実際にユーザーが読み取ったものを記述しようとしている場合)。また、文ごとに3つ以上の文字列を翻訳しようとしている場合(特に文法構造が大幅に異なる言語の場合)、グローバル化が容易になります。

何かを翻訳する予定がない場合は、Javaに組み込まれている+演算子のStringBuilderへの変換に依存します。または、JavaのStringBuilderを明示的に使用します。

5
Orion Adrian

ロギングの観点のみからの別のパースペクティブ。

このスレッドへのログインに関連する議論がたくさんあるので、答えに私の経験を追加することを考えました。誰かが役に立つかもしれません。

フォーマッタを使用してログを記録する動機は、文字列の連結を避けることにあると思います。基本的に、ログに記録しない場合は、文字列連結のオーバーヘッドは必要ありません。

ログに記録する必要がない限り、実際に連結/フォーマットする必要はありません。このようなメソッドを定義するとしましょう

public void logDebug(String... args, Throwable t) {
    if(debugOn) {
       // call concat methods for all args
       //log the final debug message
    }
}

この方法では、デバッグメッセージとdebugOn = falseの場合、cancat/formatterは実際にはまったく呼び出されません。

ただし、ここではフォーマッタの代わりにStringBuilderを使用することをお勧めします。主な動機は、そのいずれかを避けることです。

同時に、各ロギングステートメントに「if」ブロックを追加するのは好きではありません。

  • 可読性に影響します
  • 単体テストのカバレッジを減らします-すべての行がテストされていることを確認したいときに混乱します。

したがって、上記のようなメソッドでロギングユーティリティクラスを作成し、パフォーマンスヒットやそれに関連する他の問題を心配することなく、あらゆる場所で使用することを好みます。

3

Hhafezのテストを変更して、StringBuilderを含めるようにしました。 StringBuilderは、XP上のjdk 1.6.0_10クライアントを使用したString.formatより33倍高速です。 -serverスイッチを使用すると、係数が20に低下します。

public class StringTest {

   public static void main( String[] args ) {
      test();
      test();
   }

   private static void test() {
      int i = 0;
      long prev_time = System.currentTimeMillis();
      long time;

      for ( i = 0; i < 1000000; i++ ) {
         String s = "Blah" + i + "Blah";
      }
      time = System.currentTimeMillis() - prev_time;

      System.out.println("Time after for loop " + time);

      prev_time = System.currentTimeMillis();
      for ( i = 0; i < 1000000; i++ ) {
         String s = String.format("Blah %d Blah", i);
      }
      time = System.currentTimeMillis() - prev_time;
      System.out.println("Time after for loop " + time);

      prev_time = System.currentTimeMillis();
      for ( i = 0; i < 1000000; i++ ) {
         new StringBuilder("Blah").append(i).append("Blah");
      }
      time = System.currentTimeMillis() - prev_time;
      System.out.println("Time after for loop " + time);
   }
}

これは劇的に聞こえるかもしれませんが、絶対数が非常に少ないため、まれな場合にのみ関連すると考えています:100万の単純なString.format呼び出しの4秒は、ログまたは好む。

更新: コメントでsjbothaが指摘したように、StringBuilderテストは最終的な.toString()がないため無効です。

String.format(.)からStringBuilderへの正しい高速化係数は、私のマシンでは23です(-serverスイッチで16)。

2
the.duckman

Hhafezエントリの修正バージョンです。文字列ビルダーオプションが含まれています。

public class BLA
{
public static final String BLAH = "Blah ";
public static final String BLAH2 = " Blah";
public static final String BLAH3 = "Blah %d Blah";


public static void main(String[] args) {
    int i = 0;
    long prev_time = System.currentTimeMillis();
    long time;
    int numLoops = 1000000;

    for( i = 0; i< numLoops; i++){
        String s = BLAH + i + BLAH2;
    }
    time = System.currentTimeMillis() - prev_time;

    System.out.println("Time after for loop " + time);

    prev_time = System.currentTimeMillis();
    for( i = 0; i<numLoops; i++){
        String s = String.format(BLAH3, i);
    }
    time = System.currentTimeMillis() - prev_time;
    System.out.println("Time after for loop " + time);

    prev_time = System.currentTimeMillis();
    for( i = 0; i<numLoops; i++){
        StringBuilder sb = new StringBuilder();
        sb.append(BLAH);
        sb.append(i);
        sb.append(BLAH2);
        String s = sb.toString();
    }
    time = System.currentTimeMillis() - prev_time;
    System.out.println("Time after for loop " + time);

}

}

ループ391以降の時間ループ4163以降の時間ループ227以降の時間

1
ANON

これに対する答えは、特定のJavaコンパイラが生成するバイトコードを最適化する方法に大きく依存します。文字列は不変であり、理論的には、各「+」操作で新しいものを作成できます。しかし、コンパイラはほぼ確実に、長い文字列を構築するための中間ステップを最適化します。上記のコードの両方の行がまったく同じバイトコードを生成することは完全に可能です。

実際に知る唯一の方法は、現在の環境でコードを繰り返しテストすることです。両方の方法で文字列を繰り返し連結し、互いにタイムアウトする方法を確認するQDアプリを作成します。

0

連結する少数の文字列には、"hello".concat( "world!" )の使用を検討してください。他のアプローチよりもパフォーマンスが優れている可能性があります。

3つ以上の文字列がある場合は、使用するコンパイラに応じて、StringBuilderまたは単にStringの使用を検討してください。

0
Sasa