web-dev-qa-db-ja.com

String.intern()のパフォーマンスの低下

String.intern()のパフォーマンス上の利点について多くの人が話しますが、実際には、パフォーマンスのペナルティが何であるかについてもっと興味があります。

私の主な関心事は次のとおりです。

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

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

その上、他のタイプの文字列には数百万の異なる値がありますが、それでもそれぞれのコピーが数万あります(ISINコードなど)。これらの場合、100万文字列をインターンすると、基本的にintern()メソッドの速度が低下し、アプリケーションが停止するのではないかと心配しています。

41
LordOfThePigs

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

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

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

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

凡例

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

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()の使用を避けることを強くお勧めします。

39
LordOfThePigs

私は最近、String.intern()の実装に関する記事をJava 6、7、および8で作成しました: String.intern in Java 6、 7および8-文字列プーリング

-XX:StringTableSize JVMパラメーターがあります。これにより、String.internをJava7 +で非常に便利にすることができます。したがって、残念ながら、この質問は現在、読者に誤解を招く情報を提供していると言わざるを得ません。

21
mik1

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

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

YMMV。

5
bmargulies

String.internが遅くなるのは、次の2つの理由によるものです。
1。 -XX:StringTableSizeの制限。
Javaでは、内部ハッシュテーブルを使用して文字列キャッシュを管理します。Java 6、デフォルトのStringTableSize値は1009です。つまり、string.internはO(文字列の数)です。 object/1009)、ますます多くの文字列オブジェクトが作成されると、速度が低下します。

\ openjdk7\hotspot\src\share\vm\classfile\symbolTable.cpp

oop StringTable::intern(Handle string_or_null, jchar* name,  
                        int len, TRAPS) {  
  unsigned int hashValue = Java_lang_String::hash_string(name, len);  
  int index = the_table()->hash_to_index(hashValue);  
  oop string = the_table()->lookup(index, name, len, hashValue);  
  // Found  
  if (string != NULL) return string;  
  // Otherwise, add to symbol to table  
  return the_table()->basic_add(index, string_or_null, name, len,  
                                hashValue, CHECK_NULL);  
}

2. Java 6では、文字列キャッシュプールはヒープではなくパーマ領域にあります。ほとんどの場合、パーマサイズは比較的小さく設定します。

0
dingjsh