Вопрос по java, string, performance – Нарушение производительности String.intern ()

39

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

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

Search cost: The time that intern() takes to figure out if the internable string exists in the constants pool. How does that cost scale with the number of strings in that pool? Synchronization: obviously the constant pool is shared by the whole JVM. How does that pool behave when intern() is being called over and over from multiple threads? How much locking does it perform? How does the performance scale with contention?

Я обеспокоен всеми этими вещами, потому что в настоящее время я работаю над финансовым приложением, которое имеет проблему использования слишком большого количества памяти из-за дублированных строк. Некоторые строки в основном выглядят как перечисляемые значения и могут иметь только ограниченное число потенциальных значений (таких как названия валют («USD», «EUR»)), которые существуют в более чем миллионе копий. В этом случае String.intern () кажется легким делом, но я беспокоюсь о накладных расходах синхронизации вызова intern () каждый раз, когда я где-то сохраняю валюту.

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

@ skaffman Я видел этот вопрос, на который вы ссылаетесь, но он не обсуждает масштабирование производительности для стоимости поиска и не затрагивает тему синхронизации. LordOfThePigs
@ skaffman Там нет подробного анализа производительности. Marko Topolnik

Ваш Ответ

4   ответа
36

равнить String.intern () с ConcurrentHashMap.putIfAbsent (s, s). По сути, эти два метода делают одно и то же, за исключением того, что String.intern () является нативным методом, который хранит и читает из SymbolTable, который управляется непосредственно в JVM, а ConcurrentHashMap.putIfAbsent () является обычным методом экземпляра.

Вы можете найти код теста на github gist (из-за отсутствия лучшего места для этого). Вы также можете найти параметры, которые я использовал при запуске JVM (чтобы убедиться, что тест не смещен) в комментариях в верхней части исходного файла.

В любом случае вот результаты:

Стоимость поиска (однопоточная)

Легенда

Счетчик: количество отдельных строк, которые мы пытаемся объединитьininial intern: время в мс, необходимое для вставки всех строк в пул строк lookup та же строка: время в мс, необходимое для повторного поиска каждой строки из пула, используя точно такой же экземпляр, который был ранее введен в пул lookup равная строка: время в мс, необходимое для повторного поиска каждой строки из пула, но с использованием другого экземпляра

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 (), если вы собираетесь проходить более нескольких строк.

Я не стал тестировать синхронизацию. Однопоточная производительность масштабируется настолько сильно, что она делает спор точки синхронизации. LordOfThePigs
Делает Blog.codecentric.de / о / 2012/03 / ... не противоречит ли ты поиску в целом? У меня были некоторые путаницы при решении / Stackoverflow.com вопросы / 20027495 / ... Harish Kayarohanam
@ HarishKayarohanam Не думаю, что это противоречит моим выводам. Он только измеряет преимущества интернирования в памяти. Когда дело доходит до эффективности памяти, String.intern () работает как положено. С точки зрения производительности процессора, он ведет себя совсем не так, как «в основном HashSet, который хранит String в постоянном поколении». Другой ответ от mik1 дает представление о том, почему это так. В любом случае, использование HashSet, которым вы управляете самостоятельно, является более гибким и ведет себя более согласованно, не требуя настройки параметров виртуальной машины, поэтому это лучшее решение для повторного использования большого количества строк. LordOfThePigs
Вы пробовали тестировать с различными размерами бильярдного стола: Java-performance.info / строка-стажер-в-Java-6-7-8 Martin
Я этого не сделал, потому что не знал, что вы можете установить размер этой таблицы. В любом случае, String.intern () стал лучше в более поздних версиях Java, но, если это возможно, я все же предпочел бы использовать свою собственную карту. Использование вашей собственной карты дает дополнительное преимущество, заключающееся в том, что вы можете контролировать жизненный цикл ваших экземпляров и позволять им собирать мусор при необходимости. LordOfThePigs
21

7 и 8: String.intern в Java 6, 7 и 8 - объединение строк.

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

+ 1 за объяснение причин, почемуString.intern отстой, но реализация по умолчанию все еще кажется достаточно плохой в Java 7, поэтому я бы придерживался ручного объединения строк. user2357112
Чтобы просто прояснить это для других читателей, ваша статья объясняет, что пулы String.intern - это, по сути, HashSet, базовый HashMap которого имеет фиксированное количество сегментов, которое определяется параметром командной строки VM. Тем не менее, этот HashMap вроде как выродился и не знает, как увеличить количество содержащихся в нем блоков. Вот почему String.intern работает близко к O (1) для нескольких строк, но ухудшается до O (n) для гораздо большего количества строк. LordOfThePigs
5

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

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

YMMV.

Да, я имел в виду это. bmargulies
Это на самом деле не отвечает на вопрос. LordOfThePigs
-1

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

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

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

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

yeah ... За исключением того, что вы интернировали только 1 отдельную строку ... Ни у кого нет проблем с производительностью интернирования одной 1 отдельной строки. Не говоря уже о том, что ваш микропроцессор изначально недопустим, поскольку константа «E1» не только уже интернирована, но и встроена в сгенерированный байт-код. Когда я говорю, что в своем тесте я использую строку 1'000'000. Я имею в виду 1 000 000 строк, которые отличаются и для которых anyString.equals (anyOtherString) имеет значение false. Так что я не уверен, что ваш тест пытается измерить. Работа map.get с одним элементом на карте? LordOfThePigs
Да, обратите внимание, что вначале заметка «применяются обычные микро-ориентиры». Полагаю, разница будет в том, что String.intern () должен быть поточно-ориентированным, поскольку он должен читать и записывать в свой кеш, тогда как реализация Map не обязательно должна быть поточно-ориентированной, поскольку доступна только для чтения. Nathan Adams
На самом деле, нет. разница, измеряемая вашим микробенчмарком, заключается в том, что вы помещаете на карту только один элемент. Я почти уверен, что у интерна будет такая же производительность, если в пуле будет всего одна строка. LordOfThePigs
Если число строк, интернированных только из одного класса, достаточно для снижения производительности по карте, jvm делает что-то серьезно неправильно, кроме того, это доказывает, что карта лучше в этой ситуации. В любом случае, сценарий использования для карты будет специально для упомянутых валют и для любых других «строк в основном выглядят как перечисляемые значения и могут иметь только ограниченное количество потенциальных значений», а не для всех миллионов различных строк, поэтому мы в любом случае, возможно, речь идет о разных целях. Nathan Adams

Похожие вопросы