Оптимизированные реализации java.util.Map и java.util.Set? - PullRequest
14 голосов
/ 15 мая 2009

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

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

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

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

Ответы [ 17 ]

11 голосов
/ 15 мая 2009

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

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

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

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

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

7 голосов
/ 15 мая 2009

Вы смотрели на Trove4J ? С сайта:

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

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

6 голосов
/ 15 мая 2009

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

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

4 голосов
/ 15 мая 2009

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

2 голосов
/ 15 мая 2009

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

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

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

В случае, если это полезно, вот реализация Java без излишеств хеш-таблицы Numeric 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;
  }

}
2 голосов
/ 15 мая 2009

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

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

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

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

1 голос
/ 25 мая 2009

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

Забрать сообщения:

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

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

1 голос
/ 15 мая 2009

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

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

1 голос
/ 15 мая 2009

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

1 голос
/ 15 мая 2009

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

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

...