Подтвердить что ты не робот

Снижение производительности String.intern()

Многие люди говорят о преимуществах производительности String.intern(), но на самом деле меня больше интересует, какова может быть оценка исполнения.

Мои основные проблемы:

  • Стоимость поиска: время, которое занимает intern(), чтобы выяснить, существует ли строка констант в пуле констант. Как эта шкала затрат с количеством строк в этом пуле?
  • Синхронизация: очевидно, что пул констант совместно используется всей JVM. Как этот пул ведет себя, когда intern() вызывается снова и снова из нескольких потоков? Сколько блокировок оно выполняет? Как масштабируется производительность с конкуренцией?

Меня беспокоят все эти вещи, потому что я сейчас работаю над финансовым приложением, у которого есть проблема использования слишком большого количества памяти из-за дублированных строк. Некоторые строки в основном выглядят как перечисленные значения и могут иметь ограниченное количество потенциальных значений (например, названия валют ( "доллары США", "евро" )) существуют более чем в миллионах экземпляров. String.intern() в этом случае кажется неинтересным, но я беспокоюсь о накладных расходах на синхронизацию вызова intern() каждый раз, когда я где-то храню валюту.

Кроме того, некоторые другие типы строк могут иметь миллионы разных значений, но по-прежнему имеют десятки тысяч копий каждого (например, коды ISIN). Для этого я обеспокоен тем, что интернирование миллионной строки в основном замедлит метод intern(), чтобы уклониться от моего приложения.

4b9b3361

Ответ 1

Я немного поработал с бенчмаркингом. Для части стоимости поиска я решил сравнить String.intern() с ConcurrentHashMap.putIfAbsent(s, s). В принципе, эти два метода выполняют одни и те же функции, кроме String.intern() - это собственный метод, который хранит и считывает из SymbolTable, который управляется непосредственно в JVM, а 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 - количество строк в пуле. Когда количество строк в пуле растет, количество времени для поиска одной строки из пула растет намного больше (0,7 микросекунды на поиск с 10 000 строк, 40 микросекунд на поиск с 1'000'000 строк).

ConcurrentHashMap масштабируется, как ожидалось, количество строк в пуле не влияет на скорость поиска.

Основываясь на этом эксперименте, я настоятельно рекомендую избегать использования String.intern(), если вы собираетесь ставить более нескольких строк.

Ответ 2

Недавно я написал статью о реализации String.intern() в Java 6, 7 и 8: String.intern в Java 6, 7 и 8 - объединение строк.

Существует параметр -XX: StringTableSize JVM, который позволит вам сделать String.intern чрезвычайно полезным в Java7+. Поэтому, к сожалению, я должен сказать, что этот вопрос в настоящее время дает читателю вводящую в заблуждение информацию.

Ответ 3

Я нашел, что лучше использовать хэш-таблицу fastutil и выполнять мой собственный интернационал, а не повторно использовать String.intern(). Использование моей собственной хэш-таблицы означает, что я могу принять собственные решения о concurrency, и я не конкурирую за пространство PermGen.

Я сделал это, потому что я работал над проблемой, которая имела как бы миллионы строк, много одинаковых, и я хотел (а) уменьшить площадь и (б) позволить сравнивать личность. Для моей проблемы все было лучше с интернированием, чем без, используя мой подход String.intern().

YMMV.

Ответ 4

Следующий микро-тест предполагает, что использование перечисления предлагает примерно в десять раз повысить производительность (применяются обычные требования к микротестум), следующим образом:

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

Результаты (10 миллионов итераций): testIntern() - 0,8 секунды testMap() - 0,06 секунды

Конечно, YMMV, но перечисления предлагают так много преимуществ по сравнению с Strings... тип безопасности над другими случайными строками, способность добавлять методы и т.д., кажется лучшим способом пойти imho