Улучшение подсчета частот слов с помощью 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 ответов


я много работаю в НЛП и машинном обучении, поэтому мне приходится делать такие вещи все время, и есть Т возможностей для оптимизации.

несколько моментов для рассмотрения:

  1. прежде всего, вас убивает стандартный класс 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 байт.

  2. бокс и распаковка всех этих целочисленных значений заставляет ваш код работать медленнее и потреблять больше памяти. В большинстве случаев нас это не волнует, и реализация vanilla HashMap прекрасна, даже с ее обязательным боксом и жадным потреблением памяти. Но в вашем случае, если этот код выполняется в сжатые внутренний loop, мы бы предпочли специализированный класс, который знает, что его значения всегда будут целыми числами и устраняет необходимость в боксе.

  3. если вы копнете в исходный код 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;
}

в зависимости от того, что вы делаете с картой, когда вы закончите, вам может не понадобиться преобразование в карту в конце.