Оптимизированные реализации java.util.Map и java.util.Set?

Я пишу приложение, в котором важна память и, в меньшей степени, скорость. Я нашел из профилирования, что я провожу много времени в операциях Map и Set. Хотя я смотрю на способы сократить эти методы меньше, мне интересно, кто-нибудь там написал или встретится с реалиями, которые значительно улучшают время доступа или издержки памяти? или, по крайней мере, что может улучшить эти вещи, учитывая некоторые предположения?

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

Мне известно об коллекциях Commons, но я не верю, что у нее есть реализация, цель которой - быть быстрее или компактнее. То же самое для коллекций Google.

Обновление: должно быть отмечено, что мне не нужна безопасность потоков.

Ответы

Ответ 1

Обычно эти методы довольно быстрые. Есть несколько вещей, которые вы должны проверить: реализованы ли ваши хеш-коды? Являются ли они достаточно однородными? В противном случае вы получите работу с мусором.

http://trove4j.sourceforge.net/ < - это немного быстрее и сохраняет некоторую память. Я сохранил несколько мс на 50 000 обновлений

Вы уверены, что используете карты/наборы правильно? т.е. не пытаться перебирать все значения или что-то подобное. Также, например, не делайте ничего, а затем удаляйте. Просто проверьте удаление.

Также проверьте, используете ли вы Double или double. Я заметил несколько улучшений производительности нескольких десятков тысяч проверок.

Вы также правильно или правильно настроили начальную емкость?

Ответ 2

Вы посмотрели Trove4J? На веб-сайте:

Цель состоит в том, чтобы обеспечить быстрые, легкие реализации java.util.Collections API.

Контрольные показатели предоставлены здесь.

Ответ 3

Вот те, которые я знаю, в дополнение к коллекциям Google и Commons:

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

Ответ 4

Попробуйте повысить производительность ваших методов equals и hashCode, это может ускорить использование стандартных контейнеров ваших объектов.

Ответ 5

Вы можете расширить AbstractMap и/или AbstractSet в качестве отправной точки. Я сделал это не так давно, чтобы реализовать двоичную основанную на trie карту (ключ был целым числом, и каждый "уровень" на дереве был битной позицией. Left child был 0, а правый - 1). Это хорошо сработало для нас, потому что ключ был идентификатором EUI-64, и для нас большую часть времени верхние 5 байтов будут одинаковыми.

Чтобы реализовать AbstractMap, вам нужно как минимум реализовать метод entrySet(), чтобы вернуть набор Map.Entry, каждый из которых представляет собой пару ключ/значение.

Чтобы реализовать набор, вы расширяете AbstractSet и реализуете реализации size() и iterator().

Это, по крайней мере, однако. Вы также захотите реализовать get и put, так как карта по умолчанию не поддается изменению, а реализация по умолчанию получает итерации через entrySet, ища совпадение.

Ответ 6

Вы можете немного сохранить память:

(a) с использованием более сильного, более широкого хеш-кода, и, таким образом, избегая необходимости хранить ключи;

(b), выделив себя из массива, избегая создания отдельного объекта за запись хэш-таблицы.

В случае, если это полезно, здесь нетверная реализация Java из хеш-таблицы Numerical Recipies, которую я иногда нашел полезной. Вы можете напрямую подключаться к CharSequence (включая строки), иначе вы должны сами создать 64-битную хэш-функцию для ваших объектов.

Помните, что эта реализация не хранит ключи, поэтому, если два элемента имеют одинаковый хеш-код (который вы ожидаете после хэширования в порядке 2 ^ 32 или пару миллиардов если у вас есть хорошая хэш-функция), то один элемент перезапишет другой:

public class CompactMap<E> implements Serializable {
  static final long serialVersionUID = 1L;

  private static final int MAX_HASH_TABLE_SIZE = 1 << 24;
  private static final int MAX_HASH_TABLE_SIZE_WITH_FILL_FACTOR = 1 << 20;

  private static final long[] byteTable;
  private static final long HSTART = 0xBB40E64DA205B064L;
  private static final long HMULT = 7664345821815920749L;

  static {
    byteTable = new long[256];
    long h = 0x544B2FBACAAF1684L;
    for (int i = 0; i < 256; i++) {
      for (int j = 0; j < 31; j++) {
        h = (h >>> 7) ^ h;
        h = (h << 11) ^ h;
        h = (h >>> 10) ^ h;
      }
      byteTable[i] = h;
    }
  }

  private int maxValues;
  private int[] table;
  private int[] nextPtrs;
  private long[] hashValues;
  private E[] elements;
  private int nextHashValuePos;
  private int hashMask;
  private int size;

  @SuppressWarnings("unchecked")
  public CompactMap(int maxElements) {
    int sz = 128;
    int desiredTableSize = maxElements;
    if (desiredTableSize < MAX_HASH_TABLE_SIZE_WITH_FILL_FACTOR) {
      desiredTableSize = desiredTableSize * 4 / 3;
    }
    desiredTableSize = Math.min(desiredTableSize, MAX_HASH_TABLE_SIZE);
    while (sz < desiredTableSize) {
      sz <<= 1;
    }
    this.maxValues = maxElements;
    this.table = new int[sz];
    this.nextPtrs = new int[maxValues];
    this.hashValues = new long[maxValues];
    this.elements = (E[]) new Object[sz];
    Arrays.fill(table, -1);
    this.hashMask = sz-1;
  }

  public int size() {
    return size;
  }

  public E put(CharSequence key, E val) {
    return put(hash(key), val);
  }

  public E put(long hash, E val) {
    int hc = (int) hash & hashMask;
    int[] table = this.table;
    int k = table[hc];
    if (k != -1) {
      int lastk;
      do {
        if (hashValues[k] == hash) {
          E old = elements[k];
          elements[k] = val;
          return old;
        }
        lastk = k;
        k = nextPtrs[k];
      } while (k != -1);
      k = nextHashValuePos++;
      nextPtrs[lastk] = k;
    } else {
      k = nextHashValuePos++;
      table[hc] = k;
    }
    if (k >= maxValues) {
      throw new IllegalStateException("Hash table full (size " + size + ", k " + k);
    }
    hashValues[k] = hash;
    nextPtrs[k] = -1;
    elements[k] = val;
    size++;
    return null;
  }

  public E get(long hash) {
    int hc = (int) hash & hashMask;
    int[] table = this.table;
    int k = table[hc];
    if (k != -1) {
      do {
        if (hashValues[k] == hash) {
          return elements[k];
        }
        k = nextPtrs[k];
      } while (k != -1);
    }
    return null;
  }

  public E get(CharSequence hash) {
    return get(hash(hash));
  }

  public static long hash(CharSequence cs) {
    if (cs == null) return 1L;
    long h = HSTART;
    final long hmult = HMULT;
    final long[] ht = byteTable;
    for (int i = cs.length()-1; i >= 0; i--) {
      char ch = cs.charAt(i);
      h = (h * hmult) ^ ht[ch & 0xff];
      h = (h * hmult) ^ ht[(ch >>> 8) & 0xff];
    }
    return h;
  }

}

Ответ 8

Существует как минимум одна реализация в коллекциях коллекций, специально созданных для скорости: Flat3Map. Это довольно специфично, будет очень быстрым, если не более трех элементов.

Я подозреваю, что вы можете получить больше удовольствия, следуя советам @thaggie, добавьте взгляд на методы метода equals/hashcode.

Ответ 9

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

Ответ 10

Здесь есть несколько заметок и ссылки на несколько альтернативных библиотек структуры данных: http://www.leepoint.net/notes-java/data/collections/ds-alternatives.html

Я также буду голосовать за fastutil. (упоминается в другом ответе и на этой странице). Он имеет более разные структуры данных, чем вы можете встряхнуть палку, а также версии, оптимизированные для примитивных типов в виде ключей или значений. (Недостатком является то, что файл jar огромен, но вы можете предположительно обрезать его именно так, как вам нужно)

Ответ 11

Я пару лет назад прошел через нечто подобное: очень большие Карты и Наборы, а также очень многие из них. Реализация Java по умолчанию потребляла слишком много места. В конце концов, я перевернул свой собственный, но только после того, как я изучил фактические шаблоны использования, которые требовал мой код. Например, у меня был известный большой набор объектов, которые были созданы на ранней стадии, а некоторые Карты были разрежены, а другие были плотными. Другие структуры росли монотонно (без удаления), в то время как в других местах было быстрее использовать "коллекцию" и выполнять случайную, но безобидную дополнительную работу по обработке дублирующих элементов, чем тратить время и пространство на избежание дубликатов. Многие из применений, которые я использовал, были защищены от массивов и использовали тот факт, что мои хэш-коды были последовательно распределены, и, следовательно, для плотных карт поиск был всего лишь доступом к массиву.

Отнять сообщения:

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

О, и напишите модульные тесты...

Ответ 12

В то время, когда я видел, что операции "Карта" и "Набор" используют высокий процент процессора, он указал, что у меня есть более используемая карта, а "Набор" и реструктуризация моих данных почти ликвидировали коллекции от 10% -ного потребителя процессора.

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

Ответ 13

Вероятно, это не столько Map или Set, которые вызывают проблему, но и объекты позади них. В зависимости от вашей проблемы вам может понадобиться более схема типа базы данных, где "объекты" хранятся как куча байтов, а не объекты Java. Вы можете внедрить базу данных (например, Apache Derby) или сделать свою собственную специализацию. Это очень зависит от того, что вы на самом деле делаете. HashMap не преднамеренно большой и медленный...

Ответ 15

  • Commons Collections имеет идентификационную карту, которая сравнивается через ==, которая должна быть быстрее. - [Joda Primities][1], как и примитивные коллекции, как и Trove. Я экспериментировал с Trove и обнаружил, что его использование памяти лучше.
  • Я собирал коллекции многих небольших объектов с несколькими целыми. изменение их в ints позволило сохранить почти половину памяти (хотя для компенсации требуется какой-то более грязный код приложения).
  • Мне кажется разумным, что отсортированные деревья должны потреблять меньше памяти, чем хэшмапы, потому что они не требуют коэффициента загрузки (хотя, если кто-то может подтвердить или имеет причину, почему это действительно глупо, напишите в комментариях).

Ответ 16

Какую версию JVM вы используете?

Если вы не на 6 (хотя я подозреваю, что вы есть), то может помочь переключатель на 6.

Если это серверное приложение и работает в Windows, попробуйте использовать -server для использования правильной реализации хот-спота.

Ответ 17

Я использую следующий пакет (koloboke), чтобы сделать int-int hashmap, потому что он поддерживает тип promitive, и он хранит два int в длинной переменной, это классно для меня. koloboke