43

多くの人が String.intern() のパフォーマンス上の利点について語っていますが、実際には、パフォーマンスの低下がどのようなものになるかについてもっと興味があります。

私の主な懸念事項は次のとおりです。

  • 検索コスト: intern() がインターン可能な文字列が定数プールに存在するかどうかを判断するのにかかる時間。そのコストは、そのプール内の文字列の数に応じてどのように増減しますか?
  • 同期: 明らかに、定数プールは JVM 全体で共有されます。intern() が複数のスレッドから何度も呼び出されている場合、そのプールはどのように動作しますか? どのくらいのロックを実行しますか? パフォーマンスは競合によってどのように変化しますか?

私は現在、重複した文字列のためにメモリを使いすぎるという問題がある金融アプリケーションに取り組んでいるため、これらすべてのことを心配しています。一部の文字列は基本的に列挙値のように見え、限定された数の潜在的な値 (通貨名 (「USD」、「EUR」) など) しか持つことができず、100 万以上のコピーに存在します。この場合、String.intern() は非常に簡単に思えますが、通貨をどこかに保存するたびに intern() を呼び出すことによる同期のオーバーヘッドが心配です。

それに加えて、他のタイプの文字列には何百万もの異なる値が含まれる可能性がありますが、それぞれのコピーは数万に上ります (ISIN コードなど)。これらの場合、100 万個の文字列をインターンすると、基本的に intern() メソッドが遅くなり、アプリケーションが動かなくなるのではないかと心配しています。

4

5 に答える 5

39

私は自分でベンチマークを少し行いました。検索コストの部分については、String.intern()をConcurrentHashMap.putIfAbsent(s、s)と比較することにしました。基本的に、これら2つのメソッドは同じことを行いますが、String.intern()はJVMで直接管理されるSymbolTableを格納および読み取るネイティブメソッドであり、ConcurrentHashMap.putIfAbsent()は単なる通常のインスタンスメソッドです。

ベンチマークコードはgithubgistにあります(配置するのに適した場所がないため)。また、JVMを起動するときに(ベンチマークが歪んでいないことを確認するために)使用したオプションは、ソースファイルの上部にあるコメントにあります。

とにかくここに結果があります:

検索コスト(シングルスレッド)

伝説

  • count:プールしようとしている個別の文字列の数
  • 初期インターン:文字列プールにすべての文字列を挿入するのにかかった時間(ミリ秒)
  • 同じ文字列のルックアップ:以前にプールに入力されたものとまったく同じインスタンスを使用して、プールから各文字列を再度ルックアップするのにかかった時間(ミリ秒)
  • 等しい文字列のルックアップ:プールから各文字列を再度ルックアップするのにかかった時間(ミリ秒)、ただし異なるインスタンスを使用

String.intern()

count       initial intern   lookup same string  lookup equal string
1'000'000            40206                34698                35000
  400'000             5198                 4481                 4477
  200'000              955                  828                  803
  100'000              234                  215                  220
   80'000              110                   94                   99
   40'000               52                   30                   32
   20'000               20                   10                   13
   10'000                7                    5                    7

ConcurrentHashMap.putIfAbsent()

count       initial intern   lookup same string  lookup equal string
1'000'000              411                  246                  309
  800'000              352                  194                  229
  400'000              162                   95                  114
  200'000               78                   50                   55
  100'000               41                   28                   28
   80'000               31                   23                   22
   40'000               20                   14                   16
   20'000               12                    6                    7
   10'000                9                    5                    3

検索コストの結論:String.intern()は、呼び出すのに驚くほどコストがかかります。これは、O(n)のようなもので、非常にひどくスケーリングします。ここで、nはプール内の文字列の数です。プール内の文字列の数が増えると、プールから1つの文字列をルックアップする時間ははるかに長くなります(10,000文字列のルックアップあたり0.7マイクロ秒、1'000'000ストリングのルックアップあたり40マイクロ秒)。

ConcurrentHashMapは期待どおりにスケーリングされ、プール内の文字列の数はルックアップの速度に影響を与えません。

この実験に基づいて、複数の文字列をインターンする場合は、String.intern()の使用を避けることを強くお勧めします。

于 2012-05-17T01:19:36.767 に答える
5

再利用するよりも、fastutilハッシュテーブルを使用して独自のインターンを行う方がよいことがわかりましたString.intern()。自分のハッシュテーブルを使用するということは、並行性について自分で決定できることを意味し、PermGenスペースをめぐって競合することはありません。

これを行ったのは、いわば何百万もの文字列があり、多くが同一である問題に取り組んでいたためです。(a)フットプリントを減らし、(b)IDによる比較を可能にしたかったのです。私の問題については、私のnotString.intern()アプローチを使用して、インターンを使用しないよりもインターンを使用した方がうまくいきました。

YMMV。

于 2012-05-16T18:16:30.633 に答える
-1

次のマイクロ ベンチマークは、次のように列挙型を使用すると、約 10 倍のパフォーマンス向上が得られることを示唆しています (通常のマイクロ ベンチマークの警告が適用されます)。

public class Test {
   private enum E {
      E1;
      private static final Map<String, E> named = new HashMap<String, E>();
      static {
         for (E e : E.values()) {
            named.put( e.name(), e );
         }
      }

      private static E get(String s) {
         return named.get( s );
      }
   }

   public static void main(String... strings) {
      E e = E.get( "E1" ); // ensure map is initialised

      long start = System.nanoTime();
      testMap( 10000000 );
      long end = System.nanoTime();

      System.out.println( 1E-9 * (end - start) );
   }

   private static void testIntern(int num) {
      for (int i = 0; i < num; i++) {
         String s = "E1".intern();
      }
   }

   private static void testMap(int num) {
      for (int i = 0; i < num; i++) {
         E e = E.get( "E1" );
      }
   }
}

結果 (1000 万回の反復): testIntern() - 0.8 秒 testMap() - 0.06 秒

もちろんYMMVですが、列挙型は文字列よりも多くの利点を提供します...他のランダムな文字列よりもタイプセーフであり、メソッドを追加する機能などは、私見に行くための最良の方法のようです

于 2014-05-19T11:34:07.813 に答える