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


215

ログ出力などのために、常に文字列を構築する必要があります。JDKバージョンでStringBufferは、(多くの追加、スレッドセーフ)とStringBuilder(多くの追加、非スレッドセーフ)をいつ使用するかを学びました。

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

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

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);

注:私の具体的な使用例は、コード全体で何百もの「ワンライナー」ログ文字列です。彼らはループを含まないので、StringBuilderヘビー級です。String.format()特に興味があります。


28
テストしてみませんか?
Ed S.

1
この出力を生成している場合、人間が読むことができる速度と同じくらい人間が読める必要があると思います。1秒あたり最大10行としましょう。私はあなたがどちらのアプローチを取るかは本当に問題ではないと思うと思います、それが概念的に遅い場合、ユーザーはそれを評価するかもしれません。;)いいえ、ほとんどの状況でStringBuilderは重いものではありません。
Peter Lawrey、

9
@ピーター、いや、それは絶対に人間がリアルタイムで読むためのものではありません!問題が発生した場合の分析に役立ちます。ログ出力は通常、毎秒数千行になるため、効率的である必要があります。
エア

5
1秒あたり数千行を生成する場合は、1)プレーンテキスト(CSV)やバイナリなどのテキストがない場合でも短いテキストを使用する2)文字列をまったく使用しない場合は、作成せずにByteBufferにデータを書き込むことができますオブジェクト(テキストまたはバイナリ)3)ディスクまたはソケットへのデータの書き込みの背景。1秒あたり約100万行を維持できるはずです。(基本的には、ディスクサブシステムが許可する限り)10倍のバーストを達成できます。
Peter Lawrey、2009

7
これは一般的なケースとは関係ありませんが、特にロギングについては、LogBack(元のLog4j作成者が作成)には、この正確な問題に対処するパラメーター化されたロギングの形式があります-logback.qos.ch/manual/architecture.html#ParametrizedLogging
Matt Passell、2010

回答:


122

私はテストする小さなクラスを書いて、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は、最初に正規表現を使用して入力を解析してから、パラメーターを入力するためです。一方、plusとの連結は、(JITではなく)javacによって最適化され、StringBuilder.append直接使用されます。

ランタイム比較


12
このテストには、すべての文字列フォーマットを完全に適切に表現するものではないという欠点があります。多くの場合、何を含めるかに関するロジックと、特定の値を文字列にフォーマットするロジックがあります。実際のテストでは、実際のシナリオを検討する必要があります。
オリオンエイドリアン

9
+についての別の質問が+とStringBufferの比較にありました。Javaの最近のバージョンでは+可能な場合はStringBufferに置き換えられたため、パフォーマンスは変わらないでしょう
hhafez

25
これは、非常に役に立たない方法で最適化されるマイクロベンチマークによく似ています。
デビッドH.クレメンツ

20
別の不十分に実装されたマイクロベンチマーク。どちらの方法も桁違いにスケーリングするのですか。100、1000、10000、1000000、操作の使用についてはどうでしょう。分離されたコアで実行されていないアプリケーションで1桁のテストのみを実行する場合。コンテキストスイッチ、バックグラウンドプロセスなどへの「副作用」として償却することができる方法の違いの多く指示する方法はありません
エヴァンアカガレイ

8
また、あなたが今までメインJITのうち取得しないように蹴ることができません。
月Zyka

241

私はhhafezコードを取り、メモリテストを追加しました:

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回の実行ごと):
アプローチ時間(ms)割り当てられたメモリ(長い)
'+'演算子747 320,504
String.format 16484 373,312
StringBuilder 769 57,344

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

また、メッセージを作成する前に、ログレベルを確認することを忘れないでください。

結論:

  1. StringBuilderを使い続けます。
  2. 時間がない、人生が短い。

8
「メッセージを作成する前にログレベルを確認することを忘れないでください」というのは良いアドバイスです。これは、少なくともデバッグメッセージに対して行う必要があります。大量のメッセージが存在する可能性があり、本番環境では有効にすべきではないためです。
stivlo、2011年

39
いいえ、これは正しくありません。率直に言って申し訳ありませんが、それが引き付けた賛成票の数は驚くべきものです。+演算子を使用すると、同等のStringBuilderコードにコンパイルされます。このようなマイクロベンチマークはパフォーマンスを測定する良い方法ではありません-なぜjvisualvmを使用しないのか、理由のためにjdkにあります。String.format() ます遅くなりますが、フォーマット文字列ではなく、任意のオブジェクトの割り当てを解析するための時間が原因です。ロギングアーティファクトの作成を、それらが必要である確信するまで延期すること良いアドバイスですが、パフォーマンスに影響がある場合は、間違った場所にあります。
CurtainDog 2013

1
@CurtainDog、あなたのコメントは4年前の投稿に対して行われました。ドキュメントを参照するか、違いに対処するために別の回答を作成できますか?
kurtzbot 2014

1
@CurtainDogのコメントをサポートする参照:stackoverflow.com/a/1532499/2872712。つまり、ループ内で行われない限り、+が推奨されます。
アプリコット2016年

And a note, BTW, don't forget to check the logging level before constructing the message.良いアドバイスではありません。java.util.logging.*具体的には、ログレベルを確認することは、プログラムが適切なレベルにログをオンにしていない場合、プログラムに望ましくない悪影響を与える可能性がある高度な処理を行うことについて話していることを前提としています。文字列の書式設定は、そのような種類の処理ではありません。書式設定はjava.util.loggingフレームワークの一部であり、フォーマッタが呼び出される前に、ロガー自体がロギングレベルをチェックします。
searchengine27

30

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

誰も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仮想マシンが使用されました。

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


5
「+」と「フォーマット」のどちらに注釈を付ければ、これははるかに簡単に解釈できます。
AjahnCharles 2017

21

古い醜いスタイルは、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の作成、入力フォーマット文字列の解析、StringBuilderの作成、それにすべてを追加してtoString()を呼び出すため、はるかに重いです。


読みやすさの点で、投稿したコードはString.format( "%dに%dを乗算すると何が得られるか?"、varSix、varNine)よりもずっと面倒です。
dusktreader

12
+StringBuilder確かに違いはありません。残念ながら、このスレッドの他の回答には多くの誤った情報があります。質問をに変更したくなりますhow should I not be measuring performance
CurtainDog 2013

12

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

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

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

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

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


5
フォーマット文字列処理の最適化についてのあなたの推測は正しいとは思いません。Java 7を使用した実際のテストString.formatでは、内部ループ(数百万回実行)で使用すると、実行時間の10%以上がで費やされることがわかりましたjava.util.Formatter.parse(String)。これは、内部ループでは、呼び出しFormatter.formatやそれを呼び出すものPrintStream.format(Javaの標準ライブラリであるIMOの欠陥、特に解析されたフォーマット文字列をキャッシュできないため)を避ける必要があることを示しているようです。
Andy MacKinlay 14

8

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

上記のすべてを見ると、パフォーマンスに関しては、String.formatと単純な連結のどちらを使用するかが優先されます。連結よりも.formatの呼び出しを確認したい場合は、必ずそれを使用してください。
結局のところ、コードは書かれたよりもずっと多く読み込まれます。


1
パフォーマンスの点では、String.formatと単純な連結のどちらを使用するかは、あなたが好むものだと思います。これは正しくないと思います。パフォーマンスに関しては、連結の方がはるかに優れています。詳細については、私の回答をご覧ください。
Adam Stelmaszczyk 2015年

6

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

また、特定のフォーマットを頻繁に使用している場合は、Formatter()クラスを直接使用できることを忘れないでください(すべてのString.format()が行うのは、使い捨ての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番目の形式は、同時に他のことを同時に実行する場合におそらくより便利です。


8
「関連する質問」を指摘する価値があるのは実際にはC#であるため、適用されません。
エア

メモリの断片化を測定するためにどのツールを使用しましたか?断片化はRAMの速度に違いをもたらしますか?
kritzikratzi

substringメソッドがJava 7 +から変更されたことを指摘する価値があります。これで、部分文字列の文字のみを含む新しい文字列表現が返されます。コール文字列を返す必要がないことを意味する::新しいもの
ジョアン・Rebelo

5

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

何も翻訳するつもりがない場合は、Javaの組み込みの+演算子からへの変換を利用してくださいStringBuilder。または、JavaをStringBuilder明示的に使用します。


3

ロギングの観点からのみの別の視点。

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

フォーマッターを使用したロギングの動機は、文字列の連結を回避することにあると思います。基本的に、ログに記録しないのであれば、文字列連結のオーバーヘッドが発生することは望ましくありません。

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

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」ブロックを追加したくないので、

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

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


パラメータ化されたロギング機能でこのユースケースに対処することを目的とするslf4j-apiのような既存のライブラリを活用できますか?slf4j.org/faq.html#logging_performance
ammianus

2

StringBuilderを含むようにhhafezのテストを変更しました。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が指摘したように、finalがないため、StringBuilderテストは無効.toString()です。

からString.format(.)への正しいスピードアップファクターStringBuilderは、私のマシンでは23(-serverスイッチ付きでは16 )です。


1
ループするだけで消費される時間を考慮に入れていないため、テストは無効です。これを含めて、他のすべての結果から少なくとも差し引く必要があります(そうであれば、かなりの割合になる可能性があります)。
cletus 2009

私はそうしました、forループは0ミリ秒かかります。しかし、時間がかかったとしても、これは要因を増やすだけです。
the.duckman 2009

3
StringBuilderテストは、使用できるStringを実際に提供するために最後にtoString()を呼び出さないため、無効です。これを追加した結果、StringBuilderには+とほぼ同じ時間がかかります。アペンドの数を増やすと、最終的には安くなるでしょう。
Sarel Botha

1

これは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);

}

}

forループ後の時間391 forループ後の時間4163 forループ後の時間227


0

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

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


1
2番目の例のバイトコードは確実に String.formatを呼び出しますが、単純な連結が行われた場合は恐ろしいことです。なぜコンパイラはフォーマット文字列を使用するのでしょうか?
ジョンスキート、

「バイナリコード」と言ったほうがいいのに「バイトコード」を使いました。すべてがjmpとmovsになると、まったく同じコードになる可能性があります。
はい-そのジェイク。

0

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

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.