Улучшение подсчета частот слов с помощью hashmap
для одного из моих приложений следующая функция должна вызываться очень часто. Эта функция занимает много процессора, и поэтому мне интересно, Знаете ли вы, как улучшить производительность.
код подсчитывает вхождения комбинации из четырех символов. Во время тестирования я обнаружил, что количество записей на карте составляет около 100. Длина текст находится в диапазоне от 100 до 800. Начальный размер 200-это предположение, и код, похоже, работает быстрее чем без указания начального размера. Однако это, вероятно, не оптимальное значение.
private Map<String, Integer> getTetagramCount(final String text) {
final Map<String, Integer> cipherTetagrams = new HashMap<String, Integer>(200);
for (int i = 0; i < text.length() - 4; i++) {
final String tet = text.substring(i, i + 4);
final Integer count = cipherTetagrams.get(tet);
if (count != null) {
cipherTetagrams.put(tet, count + 1);
} else {
cipherTetagrams.put(tet, 1);
}
}
return cipherTetagrams;
}
6 ответов
я много работаю в НЛП и машинном обучении, поэтому мне приходится делать такие вещи все время, и есть Т возможностей для оптимизации.
несколько моментов для рассмотрения:
-
прежде всего, вас убивает стандартный класс JDK HashMap. Это хороший контейнер для вычислений общего назначения, но он ужасен для высокопроизводительных вычислений. Для каждой записи в вашей коллекции (а четырехсимвольная строка (8 байт) и целое число (4 байта), стандартная java HashMap будет потреблять:
- строковый объект
- 8-байтовый объект надземный
- 4-байтовая ссылка на массив
- 4-байтовое поле длины строки
- массив символов
- 8-байтовый объект надземный
- 2 байта для каждого символа (умножить на 4 символа) = 8 байт
- 4-байтовая длина массива поле
- целочисленный объект
- 8-байтовый объект надземный
- 4-байтовое значение int
- HashMap.Объект ввода
- 8-байтовый объект надземный
- 4-байтовая ссылка на ключ
- 4-байтовая ссылка на значение
таким образом, ваши крошечные 12 байтов данных становятся 64 байтами. И это до того, как HashMap выделил массив хэш-значений для используйте во время операций поиска. Имейте в виду, что все эти крошечные маленькие объекты означают больше работы для GC, но что более важно, это означает, что ваши объекты охватывают больший объем основной памяти и с меньшей вероятностью вписываются в кэш процессора. Когда у вас много пропусков кэша, вы теряете производительность.
Примечание: комментатор напомнил мне, что все подстроки будут иметь один и тот же базовый массив символов, что является хорошим моментом, о котором я забыл. Но все же ... означает, что каждая запись карты идет от 64 байтов до 44 байтов. Что все еще позор, когда должно быть только 12 байт.
- строковый объект
бокс и распаковка всех этих целочисленных значений заставляет ваш код работать медленнее и потреблять больше памяти. В большинстве случаев нас это не волнует, и реализация vanilla HashMap прекрасна, даже с ее обязательным боксом и жадным потреблением памяти. Но в вашем случае, если этот код выполняется в сжатые внутренний loop, мы бы предпочли специализированный класс, который знает, что его значения всегда будут целыми числами и устраняет необходимость в боксе.
если вы копнете в исходный код JDK, вы увидите, что ваш код в конечном итоге вызовет строку
hashCode()
и
вы можете попробовать реализации дерево префиксов (trie) как структура данных, особенно если вы знаете диапазон символов. Это будет не более 4 уровней, давая вам потенциально постоянное (и более быстрое постоянное) время. Как это будет работать по сравнению с hashmap действительно зависит от данных, которые у вас есть.
редактировать
альтернативно, опять же, если вы знаете диапазон символов, вы можете просто поместить их в гораздо более быстрые данные тип.
поскольку вы знаете, что все ваши символы находятся между A и Z или 0 и 9, вы можете сжать это в 6 бит каждый:
public int index(String str, int startPos) {
return
((str.charAt(startPos+3) - '0') << 18) +
((str.charAt(startPos+2) - '0') << 12) +
((str.charAt(startPos+1) - '0') << 6) +
(str.charAt(startPos) - '0');
}
//...
int[] counts = new int[42*42*42*42];
final int max = text.length() - 4;
for ( int i = 0; i < max; i++ ) {
counts[index(text, i)]++;
}
редактировать: обновлен пример выше, чтобы покрыть A-Z, 0-9
. Теперь обратите внимание на две вещи: Во-первых, вы должны создать большой массив, но вам не нужно делать это каждый раз (вы должны очистить его каждый раз!). Во-вторых, это обеспечивает очень быстрый поиск количества вхождений определенного слова, но если вы хотите повторить все слова (скажем, чтобы найти все слова, которые на самом деле произошли), что занимает O(42^4)
времени.
Ну, один из потенциальных вариантов-изменить использование неизменяемые тип обертки к изменяемому:
public final class Counter
{
private int value;
public int getValue()
{
return value;
}
public void increment()
{
value++;
}
}
затем измените ваш код:
private Map<String, Counter> getTetagramCount(final String text) {
final Map<String, Counter> cipherTetagrams = new HashMap<String, Counter>(200);
// Micro-optimization (may well not help) - only take the
// length and subtract 4 once
int lastStart = text.length() - 4;
for (int i = 0; i < lastStart; i++) {
final String tet = text.substring(i, i + 4);
Counter counter = cipherTetagrams.get(tet);
if (counter == null) {
counter = new Counter();
cipherTetagrams.put(tet, counter);
}
counter.increment();
}
return cipherTetagrams;
}
таким образом, вы только когда-либо "ставили" значение, связанное со словом один раз... после этого вы увеличиваете его на месте.
(вы могли бы потенциально использовать AtomicInteger
вместо Counter
Если вы хотите использовать встроенный тип.)
Big-O оптимизация в сторону (если есть), есть очень простой способ значительно ускорить ваше приложение: используйте что-то, чем Java API по умолчанию, которые являются очень медленно, когда дело доходит до борьбы с много данных.
заменить:
Map<String, Counter>
С Trove (что означает, что вы должны добавить банку Trove в свой проект):
TObjectIntHashMap<String>
и:
final Integer count = cipherTetagrams.get(tet);
С:
final int count = cipherTetagrams.get(tet);
потому что когда ты работаешь с много данных, используя обертки, такие как Integer вместо примитивов (например, int), и используя API Java по умолчанию, это самый верный способ застрелиться в ногу.
Я даже не начал анализировать ваш код, и я заметил, что этот метод не работает ни с какими полями-членами и может быть сделан статическим. Статические методы всегда будут работать лучше, чем нестатические методы. Я буду искать другие проблемы через минуту...
Я не уверен, что это будет быстрее, но я чувствую, что это будет.
private Map<String, Integer> getTetagramCount( final String text) {
final List<String> list = new ArrayList<String>();
for( int i =0; i < text.length() - 4; i++) {
list.add( text.substring( i, i+4);
}
Collections.sort( list);
Map<String, Integer> map = new HashMap<String, Integer>( list.size());
String last = null;
int count = 0;
for( String tetagram : list) {
if( tetagram != last && last != null) {
map.put( tetagram, count);
count = 0;
}
count++;
last = tetagram;
}
if( tetagram != null) {
map.put( tetagram, count);
}
return map;
}
в зависимости от того, что вы делаете с картой, когда вы закончите, вам может не понадобиться преобразование в карту в конце.