Медленная реализация gcc std :: unordered_map? Если да, то почему?

100

Мы разрабатываем высокопроизводительное критически важное программное обеспечение на C ++. Там нам нужна параллельная хеш-карта и реализованная. Итак, мы написали тест, чтобы выяснить, насколько медленнее наша параллельная хэш-карта по сравнению с std::unordered_map.

Но, std::unordered_mapкажется, невероятно медленно ... Так что это наш микро-тест (для одновременного отображения мы породили новую нить , чтобы убедиться , что сделать замок не получить оптимизированный прочь и к сведению , что я никогда не INSER 0 , потому что я также тест с google::dense_hash_map, которому требуется нулевое значение):

boost::random::mt19937 rng;
boost::random::uniform_int_distribution<> dist(std::numeric_limits<uint64_t>::min(), std::numeric_limits<uint64_t>::max());
std::vector<uint64_t> vec(SIZE);
for (int i = 0; i < SIZE; ++i) {
    uint64_t val = 0;
    while (val == 0) {
        val = dist(rng);
    }
    vec[i] = val;
}
std::unordered_map<int, long double> map;
auto begin = std::chrono::high_resolution_clock::now();
for (int i = 0; i < SIZE; ++i) {
    map[vec[i]] = 0.0;
}
auto end = std::chrono::high_resolution_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end - begin);
std::cout << "inserts: " << elapsed.count() << std::endl;
std::random_shuffle(vec.begin(), vec.end());
begin = std::chrono::high_resolution_clock::now();
long double val;
for (int i = 0; i < SIZE; ++i) {
    val = map[vec[i]];
}
end = std::chrono::high_resolution_clock::now();
elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end - begin);
std::cout << "get: " << elapsed.count() << std::endl;

(РЕДАКТИРОВАТЬ: весь исходный код можно найти здесь: http://pastebin.com/vPqf7eya )

Результат для std::unordered_map:

inserts: 35126
get    : 2959

Для google::dense_map:

inserts: 3653
get    : 816

Для нашей поддерживаемой вручную параллельной карты (которая выполняет блокировку, хотя тест является однопоточным, но в отдельном потоке создания):

inserts: 5213
get    : 2594

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

inserts: 4441
get    : 1180

Я компилирую с помощью следующей команды:

g++-4.7 -O3 -DNDEBUG -I/tmp/benchmap/sparsehash-2.0.2/src/ -std=c++11 -pthread main.cc

Так что особенно вставки на std::unordered_mapкажутся чрезвычайно дорогими - 35 секунд против 3-5 секунд на других картах. Также время поиска кажется довольно большим.

Мой вопрос: почему это? Я прочитал еще один вопрос о stackoverflow, где кто-то спрашивает, почему std::tr1::unordered_mapон медленнее, чем его собственная реализация. Там самый высокий ответ гласит, что std::tr1::unordered_mapнеобходимо реализовать более сложный интерфейс. Но я не вижу этого аргумента: мы используем подход ведра в нашем concurrent_map, также std::unordered_mapиспользуется подход ведра ( google::dense_hash_mapнет, но чем он std::unordered_mapдолжен быть, по крайней мере, таким же быстрым, как наша безопасная для параллелизма версия с ручной поддержкой?). Кроме того, я не вижу в интерфейсе ничего, что заставляет функцию, которая заставляет хэш-карту работать плохо ...

Итак, мой вопрос: правда ли, что это std::unordered_mapкажется очень медленным? Если нет: что не так? Если да, то с чем это связано.

И мой главный вопрос: почему вставлять значение в std::unordered_mapтакую ​​ужасно дорогостоящую (даже если мы зарезервируем достаточно места в начале, это не будет работать намного лучше - поэтому перефразирование, похоже, не проблема)?

РЕДАКТИРОВАТЬ:

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

На данный момент большинство комментариев объясняют, что я могу сделать unordered_map быстрее, предварительно выделив для него достаточно места. В нашем приложении это просто невозможно: мы разрабатываем систему управления базой данных, и нам нужна хеш-карта для хранения некоторых данных во время транзакции (например, информации о блокировках). Таким образом, эта карта может быть чем угодно, от 1 (пользователь просто делает одну вставку и фиксирует) до миллиардов записей (если происходит полное сканирование таблицы). Здесь просто невозможно заранее выделить достаточно места (и простое выделение большого количества вначале потребует слишком много памяти).

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

Так в чем же причина такой плохой работы std::unordered_map? Или другой вопрос: можно ли написать реализацию std::unordered_mapинтерфейса, которая соответствует стандарту и (почти) так же быстро, как плотная хеш-карта googles? Или в стандарте есть что-то, что заставляет разработчика выбирать неэффективный способ его реализации?

РЕДАКТИРОВАТЬ 2:

Путем профилирования я вижу, что много времени уходит на целочисленные деления. std::unordered_mapиспользует простые числа для размера массива, в то время как другие реализации используют степень двойки. Почему std::unordered_mapиспользуются простые числа? Чтобы работать лучше, если хеш плохой? Для хороших хешей это не имеет значения.

РЕДАКТИРОВАТЬ 3:

Это числа для std::map:

inserts: 16462
get    : 16978

Таааааааааааааааааааааааааааа более йлатой)): почему вставки в a std::mapбыстрее, чем вставки в std::unordered_map... Я про WAT? std::mapимеет худшую локальность (дерево против массива), ему нужно делать больше распределений (за вставку против за повтор + плюс ~ 1 для каждого столкновения) и, что наиболее важно: имеет другую алгоритмическую сложность (O (logn) против O (1))!

Маркус Пильман
источник
1
Большинство контейнеров в std ОЧЕНЬ консервативны в своих оценках, я бы посмотрел на количество ведер, которое вы используете (указанное в конструкторе), и увеличил бы его до более точной оценки для вашего SIZE.
Ylisar
Вы пробовали concurrent_hash_map от Intel TBB? threadingbuildingblocks.org/docs/help/reference/…
MadScientist,
1
@MadScientist Мы рассматривали TBB. Проблема заключается в лицензировании: это исследовательский проект, и мы еще не уверены, как мы его опубликуем (определенно с открытым исходным кодом, но если мы хотим разрешить использование в коммерческом продукте, GPLv2 будет слишком ограничивающим). Также это еще одна зависимость. Но, возможно, мы будем использовать его в более поздний момент времени, пока мы сможем хорошо жить без него.
Маркус Пильман
1
Запуск его под профилировщиком, например valgrind, может быть полезным.
Максим Егорушкин
1
Местоположение в хеш-таблице в лучшем случае немного лучше, чем местоположение в дереве, по крайней мере, если хеш-функция является «случайной». Эта хеш-функция гарантирует, что вы редко получаете доступ к ближайшим объектам в ближайшее время. Единственное преимущество, которое у вас есть, состоит в том, что массив хеш-таблицы представляет собой один непрерывный блок. В любом случае это может быть верно для дерева, если куча не фрагментирована и вы строите дерево сразу. Если размер больше, чем размер кеша, различия в локализации практически не повлияют на производительность.
Steve314

Ответы:

87

Я нашел причину: это проблема gcc-4.7 !!

С gcc-4.7

inserts: 37728
get    : 2985

С gcc-4.6

inserts: 2531
get    : 1565

Итак, std::unordered_mapв gcc-4.7 не работает (или моя установка, которая является установкой gcc-4.7.0 на Ubuntu - и другой установкой, которая является gcc 4.7.1 при тестировании debian).

Я отправлю отчет об ошибке ... до тех пор: НЕ используйте std::unordered_mapс gcc 4.7!

Маркус Пильман
источник
Есть ли что-нибудь в дельте от 4.6, что могло бы вызвать это?
Mark Canlas
30
В списке рассылки уже есть отчет. Обсуждение, похоже, указывает на «исправления» в max_load_factorобработке, которые привели к разнице в производительности.
jxh
Плохое время для этой ошибки! У меня была очень низкая производительность с unordered_map, но я рад, что об этом сообщили и "исправили".
Бо Лу
+1 - Какой отстой BBBBBUG .. Интересно, что происходит с gcc-4.8.2
ikh
2
Есть обновления по этой ошибке? Он все еще существует для более поздних версий GCC (5+)?
rph
21

Я предполагаю, что вы неправильно unordered_mapвыбрали размер , как предложил Илизар. Когда цепочки становятся слишком длинными unordered_map, реализация g ++ автоматически перекэшируется в более крупную хеш-таблицу, и это сильно снижает производительность. Если я правильно помню, по unordered_mapумолчанию (наименьшее простое число больше) 100.

У меня не было chronoв моей системе, поэтому я рассчитал times().

template <typename TEST>
void time_test (TEST t, const char *m) {
    struct tms start;
    struct tms finish;
    long ticks_per_second;

    times(&start);
    t();
    times(&finish);
    ticks_per_second = sysconf(_SC_CLK_TCK);
    std::cout << "elapsed: "
              << ((finish.tms_utime - start.tms_utime
                   + finish.tms_stime - start.tms_stime)
                  / (1.0 * ticks_per_second))
              << " " << m << std::endl;
}

Я использовал SIZEоф 10000000, и мне пришлось немного изменить кое-что для моей версии boost. Также обратите внимание, что я предварительно определил размер хеш-таблицы, чтобы она соответствовала SIZE/DEPTH, где DEPTHэто оценка длины цепочки ведра из-за хеш-коллизий.

Изменить: Ховард указывает мне в комментариях, что максимальный коэффициент нагрузки unordered_mapсоставляет 1. Итак, DEPTHконтролируется, сколько раз код будет перефразироваться.

#define SIZE 10000000
#define DEPTH 3
std::vector<uint64_t> vec(SIZE);
boost::mt19937 rng;
boost::uniform_int<uint64_t> dist(std::numeric_limits<uint64_t>::min(),
                                  std::numeric_limits<uint64_t>::max());
std::unordered_map<int, long double> map(SIZE/DEPTH);

void
test_insert () {
    for (int i = 0; i < SIZE; ++i) {
        map[vec[i]] = 0.0;
    }
}

void
test_get () {
    long double val;
    for (int i = 0; i < SIZE; ++i) {
        val = map[vec[i]];
    }
}

int main () {
    for (int i = 0; i < SIZE; ++i) {
        uint64_t val = 0;
        while (val == 0) {
            val = dist(rng);
        }
        vec[i] = val;
    }
    time_test(test_insert, "inserts");
    std::random_shuffle(vec.begin(), vec.end());
    time_test(test_insert, "get");
}

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

Я изменил код, чтобы мне было DEPTHлегче изменить его .

#ifndef DEPTH
#define DEPTH 10000000
#endif

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

elapsed: 7.12 inserts, elapsed: 2.32 get, -DDEPTH=10000000
elapsed: 6.99 inserts, elapsed: 2.58 get, -DDEPTH=1000000
elapsed: 8.94 inserts, elapsed: 2.18 get, -DDEPTH=100000
elapsed: 5.23 inserts, elapsed: 2.41 get, -DDEPTH=10000
elapsed: 5.35 inserts, elapsed: 2.55 get, -DDEPTH=1000
elapsed: 6.29 inserts, elapsed: 2.05 get, -DDEPTH=100
elapsed: 6.76 inserts, elapsed: 2.03 get, -DDEPTH=10
elapsed: 2.86 inserts, elapsed: 2.29 get, -DDEPTH=1

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

jxh
источник
6
std::unordered_mapпо умолчанию имеет максимальный коэффициент загрузки 1. Таким образом, за исключением начального количества ковшей, ваша ГЛУБИНА игнорируется. При желании можно map.max_load_factor(DEPTH).
Howard Hinnant
@HowardHinnant: Спасибо за эту информацию. Таким образом, DEPTHигнорируется, но он по-прежнему определяет, как часто карта будет перефразирована в карту большего размера. Ответ был обновлен, и еще раз спасибо
jxh
@ user315052 Да, я знаю, что могу улучшить его, придав ему разумный размер вначале - но я не могу этого сделать в нашем программном обеспечении (это исследовательский проект - СУБД - и там я не могу знать, сколько я вставлю - он может варьироваться от 0 до 1 миллиарда ...). Но даже с предварительным вызовом он медленнее, чем наша карта, и намного медленнее, чем googles density_map - мне все еще интересно, что именно имеет большое значение.
Markus Pilman
@MarkusPilman: Я не знаю, как мои результаты сравниваются с вашими, потому что вы никогда не указали, с каким размером SIZEвы работали. Я могу сказать, что unordered_mapэто в два раза быстрее с DEPTHустановленным 1и правильно предварительно выделенным.
jxh
1
@MarkusPilman: Мое время уже в секундах. Я думал, ваше время измеряется в миллисекундах. Если вставки с DEPTHустановленным значением 1занимают менее 3секунд, как это на порядок медленнее?
jxh
3

Я запустил ваш код на компьютере 64 бит / AMD / 4 ядра (2,1 ГГц) и получил следующие результаты:

MinGW-W64 4.9.2:

Использование std :: unordered_map:

inserts: 9280 
get: 3302

Использование std :: map:

inserts: 23946
get: 24824

VC 2015 со всеми известными мне флагами оптимизации:

Использование std :: unordered_map:

inserts: 7289
get: 1908

Использование std :: map:

inserts: 19222 
get: 19711

Я не тестировал код с использованием GCC, но я думаю, что он может быть сопоставим с производительностью VC, поэтому, если это правда, то GCC 4.9 std :: unordered_map все еще не работает.

[РЕДАКТИРОВАТЬ]

Так что да, как кто-то сказал в комментариях, нет оснований полагать, что производительность GCC 4.9.x будет сопоставима с производительностью VC. Когда у меня появятся изменения, я буду тестировать код на GCC.

Мой ответ - просто создать некую базу знаний для других ответов.

Кристиан Леон
источник
«Я не тестировал код с помощью GCC, но думаю, что он может быть сопоставим с производительностью VC». Абсолютно необоснованное утверждение, без каких-либо сравнительных оценок, сопоставимых с тем, что было в исходном посте. Этот «ответ» ни в каком смысле не отвечает на вопрос, не говоря уже об ответе на вопрос «почему».
4ae1e1
2
«Я не тестировал код с помощью GCC» ... как вам удалось приобрести и использовать MinGW, зная о нем так мало? MinGW, по сути, внимательно отслеживает порт GCC.
underscore_d