Подтвердить что ты не робот

Вставка и отображение вложений в С++ и производительность накладных расходов

Я хотел бы сохранить сопоставление ключа integer со значением float в памяти.

У меня примерно 130 миллионов ключей (и, соответственно, 130 миллионов значений).

Мое внимание сосредоточено на производительности поиска - мне нужно много, много миллионов поисков.

Библиотека С++ STL имеет класс map для ассоциативных массивов такого типа. У меня есть несколько вопросов о map.

Что такое накладные расходы на хранение map для набора данных указанного выше размера? Как накладные расходы на хранение, в общем, с map?

Похоже, что базовая структура данных для map - это красно-черное сбалансированное двоичное дерево. Это звучит как реальная производительность для этого O(log n) для вставки и извлечения.

Он упоминает O(1) для намеченной вставки. Мой ввод предварительно отсортирован, поэтому я считаю, что должен быть в состоянии дать подсказку о событиях вставки. Как бы я дал эту подсказку, используя перечисленные методы здесь?

Есть ли контейнер STL, который обеспечивает лучшую производительность поиска?

Существуют ли другие общедоступные структуры с открытым исходным кодом с классом ассоциированных массивов, который использует базовую структуру данных, которая будет работать лучше, чем STL map?

Если запись собственного класса контейнера обеспечит лучшую производительность поиска, какие структуры данных я могу исследовать?

Я использую GCC 4 для этой задачи, работая под Linux или Mac OS X.

Я извиняюсь заранее, если это глупые вопросы. Благодарим вас за советы.

4b9b3361

Ответ 1

Учитывая то, что вы сказали, я бы очень серьезно подумал об использовании std::vector<pair<int, float> > и использовании std::lower_bound, std::upper_bound и/или std::equal_range для просмотра до значения.

Хотя точные издержки std::map могут (и могут) изменяться, практически нет места для вопроса о том, что он обычно потребляет дополнительную память и ищет значения медленнее, чем бинарный поиск в векторе. Как вы заметили, это обычно (и почти неизбежно) реализуется как некое сбалансированное дерево, которое накладывает накладные расходы на указатели и информацию о балансировке и, как правило, означает, что каждый узел также выделяется отдельно. Поскольку ваши узлы довольно малы (обычно 8 байт), дополнительные данные, вероятно, будут как минимум такими же, как те, что вы на самом деле храните (т.е. Как минимум 100% накладных расходов). Отдельное распределение часто означает плохую локальность ссылок, что приводит к плохому использованию кэша.

В большинстве реализаций std::map используется красно-черное дерево. Если вы собираетесь использовать std::map, реализация, использующая дерево AVL, вероятно, подойдет вам лучше - дерево AVL имеет несколько более жесткие ограничения на балансировку. Это дает немного более быстрый поиск за счет немного более медленного вставки и удаления (так как он должен чаще балансировать, чтобы поддерживать более строгую интерпретацию "сбалансированного"). Пока ваши данные остаются постоянными во время использования, std::vector по-прежнему почти наверняка лучше.

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

Предполагая, что ключи достаточно равномерно распределены (или, по крайней мере, следуют некоторому предсказуемому шаблону, поддающемуся интерполяции), поиск интерполяции имеет сложность O (log log N). Для 130 миллионов ключей это составляет около 4 проб, чтобы найти предмет. Чтобы сделать это значительно лучше, чем при обычном/неидеальном хешировании, вам нужен хороший алгоритм, и вы должны держать коэффициент загрузки в таблице достаточно низким (обычно около 75% или около того), т.е. вам необходимо учитывать что-то вроде 32 миллионов дополнительных (пустых) мест в вашей таблице, чтобы увеличить ожидаемую сложность с четырех проб до трех). Возможно, я просто старомоден, но мне кажется, что для такого небольшого улучшения скорости мне нужно много дополнительного хранилища.

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

Ответ 2

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

std::map<int, float>:

1.3 million insertions
Total memory allocated: 29,859 KB
Total blocks allocated: 1,274,001
Total time: 17.5 seconds

std::vector<std::pair<int, float> >:

1.3 million insertions
Total memory allocated: 12,303 KB
Total blocks allocated: 1
Total time: 0.88 seconds

std:: map использует в два раза больше хранилища и занимает 20 раз дольше, чтобы вставить все элементы.

Ответ 3

Большинство компиляторов поставляются с нестандартным (но работающим) hash_map (или unordered_map), который может быть быстрее для вас. Он приходит в С++ 0x (находится в tr1), и он также (как всегда) в boost уже.

GCC тоже, но я не сделал С++ для этого для.. 12 лет.., но он все равно должен быть где-то там.

Ответ 4

Если ваш вход отсортирован, вы должны попробовать только вектор и двоичный поиск (т.е. lower_bound()). Это может оказаться adaquate (это также O (log n)). В зависимости от распределения ваших ключей и используемой хэш-функции может работать hash_map. Я думаю, что это tr1::unordered_map в gcc.

Ответ 5

В качестве частичного ответа на ваш вопрос, касающийся производительности поиска, вы должны рассмотреть свой шаблон вставки. Вы отметили, что std::map использует дерево Red-Black Tree в качестве хеджирования для линеаризации тщательно отсортированной вставки в связанный список. Следовательно, такое дерево обеспечивает O (log n) время поиска, несмотря на аберрантный порядок вставки. Тем не менее, вы платите за это, при установке, удалении и обходах, а также теряете местность ссылок для повторного чтения "близких" данных.

Хэш-таблица может предлагать более быстрый поиск, если вы можете настроить хэш-функцию для вашего типа ключа (целое, как вы говорите), что предотвратит столкновения. Если ваш набор данных был исправлен, чтобы вы могли его загрузить один раз и только после этого прочитать, вы можете использовать параллельные массивы целых чисел и float и использовать std::lower_bound для поиска вашего соответствия через двоичный поиск. Сортировка параллельных массивов должным образом была бы сложной задачей, если бы ваши ключи были отделены от их соответствующих значений, но вам бы понравилось более жесткое хранилище и локальность ссылок, чем сохранение массива, скажем, std::pair.

Ответ 6

Вы можете посмотреть на std:: tr1:: unorderd_map.

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

4294967296/130000000 = 33, поэтому о каждом 33-м номере во всем пространстве используется в ваших данных.

Вы можете, например, разделить диапазон ключей на разделы с фиксированным размером. Если ключи распределены довольно равномерно, вы должны разделить пространство ключа, например. Ведра размером 256 или даже 32-х мерные ведра зависят от того, сколько хранилища вы хотите тратить, когда хранится только несколько значений.

Пример, чтобы дать вам идею:

#define BUCKET_SIZE  256
#define BUCKET_SIZE_SHIFT  8
struct Bucket {
  uint32_t key;
  float value;
  Bucket* pNext;
};

Bucket data[ 4294967296 / BUCKET_SIZE ];

Bucket* find( uint32_t key ) {
  uint32_t bucket_index = key / BUCKET_SIZE;
  // or faster: uint32_t bucket_index = key >> BUCKET_SIZE_SHIFT;
  Bucket* pBucket = &data[ bucket_index ];
  while( pBucket ) {
    if( pBucket->key == key ) return pBucket;
    pBucket = pBucket->pNext;
  }
  return NULL;
}

Ответ 7

Если ваши ключи неизменны, вы можете рассмотреть идеальную хэш-функцию в качестве альтернативы стандартным контейнерам.

Я не знаю, с какими препятствиями вы столкнетесь с набором данных такого размера, но, возможно, стоит потратить несколько минут на эксперименты.

Ответ 8

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

В этом отношении предпочтительным является смешанное решение небольшого хэшмапа в качестве первого слоя и отсортированных векторов для ведер.

Идея состоит в том, чтобы сохранить индекс хэш-таблицы в кэш-памяти и искать в меньших сортированных контейнерах, чтобы уменьшить количество ошибок кэша.