StringBufferとStringのパフォーマンス


cheramiが書いた「StringかStringBufferか?」後の複数のネットユーザーのコメントでは、この問題をよく検討する必要があると感じ、簡単なテストクラスとスクリプトを作成して実行しました.そのテストクラスを異なるパラメータとして修正し,異なるJDKでテストしたところ,この問題は実に興味深い問題であることが分かった.それでは始めましょう.
第一歩、準備
その後のテスト作業を容易にするためには、簡単なスクリプトを作成する必要があります.

      echo test by jdk1.2.2
/opt/java/jdk1.2.2/bin/javac StringTest.java
/opt/java/jdk1.2.2/bin/java StringTest
echo test by jdk1.3.1_09
/opt/java/jdk1.3.1_09/bin/javac StringTest.java
/opt/java/jdk1.3.1_09/bin/java StringTest
echo test by jdk1.4.2
/opt/java/jdk1.4.2/bin/javac StringTest.java
/opt/java/jdk1.4.2/bin/java StringTest

上のスクリプトは必要に応じてwindowsやlinuxに適用できます.私はlinuxでテストしているので、ファイルstringtestとして保存します.sh Windowsでテストしたらstringtestとして保存できますbat.
注:本明細書の後続の実行結果は、連続して複数回実行され、比較平均および典型的なサンプル値の1つを取得します.
ステップ2でコードの書き込みを開始します
最初はあまり考えずに次のコードを書きました.

      public class StringTest {
  public static void main(String[] args) {
    long start=System.currentTimeMillis();
    for (int i=0; i<10000; i++) {
      String s="This is a "+"long test string for "+"different JDK  
      performance "+"testing.";    }
    long end=System.currentTimeMillis();
    System.out.println("Directly string contact:"+(end-start));      
    start=System.currentTimeMillis();
    for (int i=0; i<10000; i++) {
      StringBuffer buffer = new StringBuffer(); 
     buffer.append("This is a "); 
     buffer.append("long test string for ");
      buffer.append("different JDK performance "); 
     buffer.append("testing.");
      String ss=buffer.toString(); 
   }
    end=System.currentTimeMillis();
    System.out.println("StringBuffer contact:"+(end-start));
  }
}

実行結果:

      test by jdk1.2.2
Directly string contact:0
StringBuffer contact:120
test by jdk1.3.1_09
Directly string contact:1
StringBuffer contact:47
test by jdk1.4.2
Directly string contact:0
StringBuffer contact:53

ほほほ、意外なことが起こったのではないでしょうか?!!!私も最初はそうでしたが、焦らないでください.実は私は間違いを犯しました.文字列+操作を行うのは文字列の直接量なので、コンパイラはコンパイル時に最適化したので、String s=「This is a」+「long test string for」+「different JDK performance」+「testing.」;コンパイル後は実際にString s="This is is a long test string for different JDK performance testing.";ほほほ、これは簡単な賦値操作で、道理で使った時間はほとんどありません.
ステップ3、コードの変更

      public class StringTest {
  public static void main(String[] args) {
    String s1="This is a ";
    String s2="long test string for ";
    String s3="different JDK performance ";
    String s4="testing."; 
           long start=System.currentTimeMillis();
    for (int i=0; i<10000; i++) {
      String s=s1+s2+s3+s4;
    }
    long end=System.currentTimeMillis();
    System.out.println("Directly string contact:"+(end-start));    
    start=System.currentTimeMillis();
    for (int i=0; i<10000; i++) {
      StringBuffer buffer = new StringBuffer();
      buffer.append(s1);
      buffer.append(s2);
      buffer.append(s3);
      buffer.append(s4);
      String ss=buffer.toString();
    }
    end=System.currentTimeMillis();
    System.out.println("StringBuffer contact:"+(end-start));
  }
}

実行結果:

      test by jdk1.2.2
Directly string contact:140
StringBuffer contact:123
test by jdk1.3.1_09
Directly string contact:32
StringBuffer contact:21
test by jdk1.4.2
Directly string contact:48
StringBuffer contact:37

上の結果から見ると、確かに「Stringを使うかStringBufferを使うか」を得ることができます.という結論に達し、また、異なるJDKのバージョンの性能の違いが比較的大きいことがわかる、JDK 1.2.2の性能は最悪で、JDK 1.3.1の性能が最も優れている.
討論は終わりましたか.ほほほ、焦らないでください、まだ远く终わっていません.:)
ステップ4、サイクル数を減らす(10000回から1000になる)

      public class StringTest {
  public static void main(String[] args) {
    String s1="This is a ";
    String s2="long test string for ";
    String s3="different JDK performance ";
    String s4="testing.";
            long start=System.currentTimeMillis();
    for (int i=0; i<1000; i++) {
      String s=s1+s2+s3+s4;
    }    
long end=System.currentTimeMillis();
    System.out.println("Directly string contact:"+(end-start));    
    start=System.currentTimeMillis();
    for (int i=0; i<1000; i++) {
      StringBuffer buffer = new StringBuffer();
      buffer.append(s1);
      buffer.append(s2);
      buffer.append(s3);
      buffer.append(s4);
      String ss=buffer.toString();
    }
    end=System.currentTimeMillis();
    System.out.println("StringBuffer contact:"+(end-start));
  }
}

実行結果:

      test by jdk1.2.2
Directly string contact:12
StringBuffer contact:19
test by jdk1.3.1_09
Directly string contact:9
StringBuffer contact:13
test by jdk1.4.2
Directly string contact:12
StringBuffer contact:18

何を見た?以上の結論はまた覆され、文字列の直接接続操作の性能はStringBufferを使用するよりも優れており、そのバージョンのJDKにおいても、JDK 1においても優れている.3.1のパフォーマンスは依然として最高です.私たちは思わずなぜか聞きたい.残念ながら、私もなぜか分かりません.私の考えでは、上記の2つのプログラムの違いは主にループの回数にあり、回数の違いによって作成されたオブジェクトの個数、すなわちメモリの使用状況が異なり、もう1つの違いは、実行回数の違いによってJITが実行時に最適化する強度が異なることです.この2つの要因は、プログラムの結果に必ず影響を与える要因だと思いますが、どのように影響しているのか、私にはわかりません.
ここまで来たら、私たちの物語は終わるかもしれませんが、上の結果から見ると、もう少しテストする必要があると思います.
5つ目は、すべての中間結果を串刺しにします.

      public class StringTest {
  public static void main(String[] args) {
    String s1="This is a ";
    String s2="long test string for ";
    String s3="different JDK performance ";
    String s4="testing."; 
           long start=System.currentTimeMillis();
    String s="";
    for (int i=0; i<1000; i++) {
      s+=s1+s2+s3+s4;
    }
    long end=System.currentTimeMillis();
    System.out.println("Directly string contact:"+(end-start)); 
   start=System.currentTimeMillis();
    StringBuffer buffer = new StringBuffer();
    for (int i=0; i<1000; i++) { 
     buffer.append(s1);
      buffer.append(s2);
      buffer.append(s3);
      buffer.append(s4);
      String ss=buffer.toString();
    }
    end=System.currentTimeMillis();
    System.out.println("StringBuffer contact:"+(end-start));
  }
}

実行結果:

      test by jdk1.2.2
Directly string contact:997
StringBuffer contact:13
test by jdk1.3.1_09
Directly string contact:1900
StringBuffer contact:21
test by jdk1.4.2
Directly string contact:2157
StringBuffer contact:11

StringBufferを使う大きなメリットをやっと見ました!興味深い現象に気づくかもしれませんJDK 13.1の性能はここではそれほど際立っていないようで、StringBufferでは最悪だった.とてもおもしろいですね.
ステップ6で、サイクル数を再び減らす(1000回から500回に減らす)

      public class StringTest {
  public static void main(String[] args) {
    String s1="This is a ";
    String s2="long test string for ";
    String s3="different JDK performance ";
    String s4="testing.";
            long start=System.currentTimeMillis();
    String s="";
    for (int i=0; i<500; i++) {
      s+=s1+s2+s3+s4;
    }
    long end=System.currentTimeMillis();
    System.out.println("Directly string contact:"+(end-start));
    start=System.currentTimeMillis();
    StringBuffer buffer = new StringBuffer();
    for (int i=0; i<500; i++) {
      buffer.append(s1);
      buffer.append(s2);
      buffer.append(s3);
      buffer.append(s4);
      String ss=buffer.toString();
    }
    end=System.currentTimeMillis();
    System.out.println("StringBuffer contact:"+(end-start));
  }
}

実行結果:

      test by jdk1.2.2
Directly string contact:270
StringBuffer contact:9
test by jdk1.3.1_09
Directly string contact:251
StringBuffer contact:2
test by jdk1.4.2
Directly string contact:264
StringBuffer contact:2

ふふ、JDK 1.3.1の性能の優位性はまた帰ってきました!また,1000回の場合と比較して,文字列直接操作の性能差は減少した.
また、上記のサンプル値は1つだけですが、何度も実行するとJDK 1という現象が見られます.2.2とJDK 1.3.3の運行時間は運行するたびに差が少ないが、JDK 1.4.2のStringBuffer操作の運行時間の変動は比較的に大きくて、私の環境の下の変動範囲は意外にも1から16の間にあります!
物語はここまで来て、私はこれ以上テストを続けたくありません.私の頭の中にはもう誰がいいか悪いかの結論はありません.私たちは上の実験の結果をまとめる必要があります.
まとめ
上のいくつかの実験から見ると、次のような結論が得られると思います.
1.Stringの+操作とStringBufferには絶対的な性能の問題は存在しないが、そのバージョンのJDKにおいても、接続の内容が非常に多く、コードが実行される回数が多い場合は迷わずStringBufferを使用すると、この性能の違いが大きいと結論付けられる.接続が比較的短いコンテンツであれば、お気に入りの方法を使用しましょう.それらの性能には明らかな違いはありません.
2.異なるバージョンのJDKはこの2つの操作に対する最適化が異なる、全体的にJDK 1.3.1それらの最適化は比較的良好である.
私が注意したいのは、次のような結論を出さないことです.
JDK1.3.1の性能は最高です.私たちの結論はただ私たちがテストした問題に対して、他の問題に対して、JDK 1.3.1の表現がどうなのかは分からないが、JDK 1を信じている.3.1の全体性能比JDK 1.2.2もっといいです.
最後に注意したいことは、
1.性能の最適化を盲目的に信じないで、私が以前JRのある文章で見た言葉で言えば、それが確かに性能のボトルネックであることを知っているところでしか最適化できないということです.
2.JDKの異なるバージョンはいくつかの面でいくつかの実行とコンパイルの最適化を行いましたが、この最適化は固定されていません.私たちはそれらの最適化にあまり期待しないでください.最も根本的なのはあなたのコードの性能です.
ネットユーザーのコメント
コメント:firebox
スレ主はこのようなテストで性能の違いを示すことができないと思います.JAvaアプリケーションは単純にあなたが言ったようなものではありません.仮想マシンが長期にわたって稼働している間、メモリの管理に関連しています.+操作は明らかにオブジェクトが増加し、メモリゴミがますます多くなり、同時に要求されると、状況はもっと悪いと思います.
客:jsyx
疑いの余地のないテスト.
StringBufferの意味では接続速度だけではありません.
サーバの観点からStringBufferを用いることは,直接接続よりも少ない多くのゴミオブジェクトを生成できることを意味する.gcの実行回数/時間の低下を知るには,高速で長期にわたって実行されるプログラムにとって重要な意義がある.
コメント:wolfsquare
改めて見ると、私の個人的な経験に基づいてjvmのcurrentTimeMillis()法を用いて得られた数値は不正確で、少なくとも100ミリ秒のレベルでは不正確であることが分かった.著者は類似のテストがあれば、最も多くのことを何回かして、彼らの平均値を取ることを提案した.
コメント者:ljdrer
ほほほ、冒頭でこのテストの目的を明確に言わなかったのかもしれませんが、実は最初の目的は、プロジェクトをするときに文字列+の操作を使用できないことを要求したためです.性能が悪いし、自分も以前確かにいくつかの文章や本を見たことがあります.これは確かに問題だと言っていましたが、JDKが絶えず最適化されていることを考えて、この問題はもう存在しないかもしれません.
ここでテストの目的を補足して説明します.
テストするのは、次の点だけです.

      String s=s1+s2+s3+s4;
  StringBuffer buffer=new StringBuffer();
buffer.append(s1);
buffer.append(s2);
buffer.append(s3);
String s=buffer.toString();

間の性能の違い、プログラムが循環するのはこの違いを拡大するためで、さもなくば1つの文の実行時間の違いはどれだけ大きいことができますか?
コメント者:ljdrer
to wolfsquare:原文の冒頭ははっきり言っています.
「本明細書の後の実行結果は、連続して複数回実行され、平均および典型的なサンプル値の1つを取得します」
だからあなたの言う問題は存在しないでしょう.
コメント:mosa
The methodology you mentioned is not fair enough. Given that you only run the loop with less than some hundred milliseconds,there're many factors that may bias the results. To name a few:
1. VM initialization, class loading and JIT time, which can easily amount to some 10 ms. If you test StringBuffer first and then String "+", the result may be different. You may first run both cases with a few loops and then begin real time collecting.
2. GC may take a few ms to some 10 ms, depends on different systems. For a collecting timeframe like some 10 ms, one more GC may contaminate the expected results. You may run Runtime.gc() before the StringBuffer test begins. But the behavior of Runtime.gc() may depend on different VMs.
For a modified step 3 and 4, you can conclude String "+"and StringBuffer append are with comparable performance. That's not surprising, because after you use "javap -c"to deassemble them into bytecode, you can find the two bytecode sequences are very similar (String "+"may even be more optimal in the bytecode level, because it utilizes StringBuffer.append's return value for chained concatenation). StringBuffer has the advantage that you can specify the initial capacity with your knowledge in the context (the default value is 16?), then you can avoid many capacity expansion which further relaxes GC and arraycopy overhead.
Step 5 is totally different from 3&4. The String "+"part creates much more objects (at least one String and one StringBuffer per loop and many subsequent objects like char arrays) than StringBuffer part. "javap -c"and "java -verbosegc"may help you solve the puzzle.
客:finalarrow
StringBufferを初期インスタンス化するときにStringBufferの長さを設定してみます(もちろんテストの結果文字列より少し大きいですが)、どのような結果が得られますか?試したことはありませんが、これについて議論した文章があります.長さを設定すると、StringBufferは毎回サイズを比較してアドレスを割り当てる必要がなく、スピードがずっと速くなります.
これはVectorの原理と同じです.