Улучшение подсчета частот слов с помощью hashmap - PullRequest
7 голосов
/ 03 декабря 2010

Для одного из моих приложений, следующая функция должна вызываться очень часто.Эта функция занимает много процессорного времени, и поэтому мне интересно, знаете ли вы, как повысить производительность.

Код подсчитывает количество встреч комбинации из четырех символов.Во время тестирования я обнаружил, что количество записей на карте составляет около 100. Длина text находится в диапазоне от 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 ]

11 голосов
/ 03 декабря 2010

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

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

  1. Прежде всего, вас убивает стандартный класс JDK HashMap. Это хороший контейнер для вычислений общего назначения, но он ужасен для высокопроизводительных вычислений. Для каждой записи в вашей коллекции (строка из четырех символов (8 байтов) и целое число (4 байта)) стандартная java HashMap будет использовать:

    • строковый объект
      • 8-байтовые данные объекта
      • 4-байтовая ссылка на массив
      • 4-байтовое поле длины строки
    • массив символов
      • 8-байтовые данные объекта
      • 2 байта для каждого символа (умножено на 4 символа) = 8 байтов
      • 4-байтовое поле длины массива
    • Целочисленный объект
      • 8-байтовые данные объекта
      • 4-байтовое значение типа int
    • Объект HashMap.Entry
      • 8-байтовые данные объекта
      • 4-байтовая ссылка на ключ
      • 4-байтовое значение Ссылка

    Итак, ваши крошечные 12 байтов данных становятся 64 байтами. И это до того, как HashMap выделил массив значений хеш-функции для использования во время операций поиска. Имейте в виду, что все эти крошечные маленькие объекты означают больше работы для ГХ, но, что более важно, это означает, что ваши объекты занимают большую область основной памяти и с меньшей вероятностью помещаются в кэш процессора. Когда у вас много кешей, вы теряете производительность.

    ПРИМЕЧАНИЕ. Комментатор напомнил мне, что все подстроки будут использовать один и тот же базовый массив символов, что является хорошим моментом, о котором я забыл. Но, тем не менее, это означает, что каждая запись карты идет от 64 байтов до 44 байтов. Который все еще позор, когда должно быть только 12 байтов.

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

  3. Если вы покопаетесь в исходном коде JDK, вы увидите, что ваш код будет в итоге дважды вызывать строковые методы hashCode() и equals(). Один раз для map.get() и один раз для map.put(). Но есть другой тип коллекции, называемый HashBag , который может выполнять поиск, вставку и увеличение количества только с одним поиском. Коллекция «bag» отчасти похожа на «набор», за исключением того, что она может содержать дубликаты и отслеживает, сколько существует дубликатов. Для каждой из ваших тетраграмм вам нужно было бы просто позвонить bag.put(tetragram) без необходимости сначала получать и обновлять счет. К сожалению, в JDK нет реализаций пакетов, поэтому вам нужно найти их в другом месте или написать самому.

  4. К счастью, ваши тетраграммы можно кодировать без потерь в виде значений long (поскольку каждый символ java имеет ширину 2 байта, а long дает вам восемь байт для работы). Поэтому вы можете выполнять итерацию по массиву символов и преобразовывать каждую тетраграмму в long, а также избегать лишних затрат на создание такого количества крошечных строк. Затем вы можете сохранить свои результаты в LongIntHashMap (из библиотеки Trove). Это будет намного быстрее, чем ваша текущая реализация, потому что вы можете избежать создания всех этих крошечных строковых объектов.

  5. Хотя Trove LongIntHashMap довольно превосходен, он не так хорош, как LongHashBag.Нет вызова equals (так как long можно сравнить с оператором ==), но вы все равно заплатите цену за два вызова hashCode.Если вы хотите быть действительно агрессивным в оптимизации, вы можете посмотреть на исходный код LongIntHashMap и выяснить, как его преобразовать в LongHashBag.Это не так сложно, и, в конце концов, это именно то, что я сделал в своем собственном коде.


Обновление 1:

Хорошо, вот немногос кодом:

private LongHashBag countTetragrams(String text) {

  // Homework assignment: find a good LongHashBag implementation, or
  // grab the LongIntHashMap implementation from Trove, and tweak it
  // to work as a Bag
  LongHashBag bag = new LongHashBag(500);

  // There are no tetragrams in this string.
  if (text.length() < 4) return bag;

  // Shortcut: if we calculate the first tetragram before entering
  // the loop, then we can use bit-shifting logic within the loop
  // to create all subsequent tetragram values.
  char[] c = text.toCharArray();
  long tetragram = ((long) c[0] << 48) |
     (((long) c[1]) << 32) |
     (((long) c[2]) << 16) |
     ((long) c[3]);

  bag.add(tetragram);

  for (int i = 4, last = text.length(); i < last; i++) {
     // During each loop iteration, the leftmost 2-bytes are shifted
     // out of the tetragram, to make room for the 2-bytes from the
     // current character.
     tetragram = (tetragram << 16) | ((long) c[i]);
     bag.add(tetragram);
  }

  return bag;
}

Обновление 2:

Я только что провел несколько испытаний различных решений, и я собирался получить примерно 25% -ное улучшение производительности при использовании LongHashBagподход вместо стандартного подхода HashMap.

Тем не менее, я собирался получить улучшение на 300% путем переработки полученных объектов.По сути, вместо этого:

private LongHashBag countTetragrams(String text) {

  // Creates a new HashBag on every invocation. Very wasteful.
  LongHashBag bag = new LongHashBag(500);

  // ...blah blah blah...

  return bag;
}

... Я сейчас делаю это ...

private void countTetragrams(String text, LongHashBag bag) {

  // Return the object to a neutral state, and recycle it.
  bag.clear()

  // ...blah blah blah...
}

Вызывающий код отвечает за создание объекта LongHashBag и обеспечение того, чтобымы закончили с этим, когда мы снова вызываем метод count.

Но это также сработает ...

private LongHashBag countTetragrams(String text) {

  // Return the object to a neutral state, and recycle it.
  LongHashBag bag = retrieveLongHashBagFromObjectPool();

  // ...blah blah blah...
  return bag;
}

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

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

Но, как я уже сказал, утилизация этих объектов дает повышение производительности на 300%.

7 голосов
/ 03 декабря 2010

Вы можете попробовать реализовать дерево префиксов (trie) в качестве структуры данных, особенно если вы знаете диапазон символов. Это будет максимум 4 уровня, что даст вам потенциально постоянное (и более быстрое постоянное) время. Как это будет работать по сравнению с хэш-картой, зависит от имеющихся у вас данных.

Редактировать

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

Поскольку вы знаете, что все ваши символы находятся в диапазоне от 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) время.

4 голосов
/ 03 декабря 2010

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

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

1 голос
/ 03 декабря 2010
* * * * * * * За исключением оптимизации Big-O (если есть), существует очень простой способ значительно ускорить ваше приложение: использовать что-то, кроме API-интерфейсов Java по умолчанию, которые очень медленны, когда дело доходит дос лотом данных.

Заменить:

Map<String, Counter>

На Trove (что означает, что вы должны добавить банку Trove в свой проект):

TObjectIntHashMap<String>

И:

final Integer count = cipherTetagrams.get(tet);

с:

final int count = cipherTetagrams.get(tet);

Потому что, когда вы работаете с лотом данных, используйте обертки типа Integer вместо примитивов(например, int) и использование Java API по умолчанию - самый верный способ выстрелить себе в ногу.

0 голосов
/ 03 декабря 2010

Я не уверен, что это будет быстрее, но у меня есть ощущение, что это будет.

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

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

0 голосов
/ 03 декабря 2010

Я даже не начал анализировать ваш код и заметил, что этот метод не работает с полями-членами и может быть сделан статическим. Статические методы всегда будут работать лучше, чем нестатические. Я поищу больше вопросов через минуту ...

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...