Является ли реализация GCC std::unordered map медленной? Если так , то почему?
мы разрабатываем высокопроизводительное критическое программное обеспечение на C++. Там нам нужна параллельная хэш-карта и реализована одна. Поэтому мы написали тест, чтобы выяснить, насколько медленнее наша параллельная хэш-карта по сравнению с std::unordered_map
.
а, std::unordered_map
кажется невероятно медленным... Так что это наш микро-бенчмарк (для параллельной карты мы породили новый поток, чтобы убедиться, что блокировка не оптимизируется и обратите внимание, что я никогда не inser 0, потому что я также бенчмарк с google::dense_hash_map
, для которого требуется значение null):
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;
(EDIT: весь исходный код можно найти здесь: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
так ужасно дорого (даже если мы зарезервируем достаточно места в начале, это не будет работать намного лучше - так что перефразирование, кажется, не проблема)?
EDIT:
прежде всего: да, представленный бенчмарк не безупречен - это потому что мы много играли с ним, и это просто Хак (например,uint64
распределение для генерации ints на практике не было бы хорошей идеей, исключить 0 в цикле-это глупо и т. д...).
на данный момент большинство комментариев объясняют, что я могу сделать unordered_map быстрее, предварительно выделив для него достаточно места. В нашем приложении это просто невозможно: мы разрабатываем системы управления базами данных и нужна хэш-карта для хранения данных во время транзакции (для пример блокировки информации). Таким образом, эта карта может быть всем от 1 (пользователь просто делает одну вставку и фиксирует) до миллиардов записей (если происходит полное сканирование таблицы). Здесь просто невозможно заранее выделить достаточно места (и просто выделить много в начале будет потреблять слишком много памяти).
кроме того, я извиняюсь, что не сформулировал свой вопрос достаточно ясно: я не очень заинтересован в том, чтобы сделать unordered_map быстрым (использование плотной хэш-карты googles отлично работает для нас), я просто не совсем понимаю, откуда берутся эти огромные различия в производительности. Это не может быть просто предварительное размещение (даже с достаточным количеством предварительно выделенной памяти, плотная карта на порядок быстрее, чем unordered_map, наша рука поддерживает параллельную карту начинается с массива размером 64 - так меньше, чем unordered_map).
так в чем же причина такого неудачного выступления std::unordered_map
? Или по-другому спросил: Можно ли написать реализацию std::unordered_map
интерфейс, который является стандартным соответствовать и (почти) так же быстро, как гуглит плотной хэш-карту? Или в стандарте есть что-то, что заставляет исполнителя выбирать неэффективный способ его реализации?
EDIT 2:
профилируя, я вижу, что для целых делений используется много времени. std::unordered_map
использует простые числа, для размера массива, в то время как другие реализации полномочий двух. Почему std::unordered_map
использовать простые числа? Чтобы лучше работать, если хэш плохой? Для хороших хэшей это имхо не делает разница.
EDIT 3:
это цифры для std::map
:
inserts: 16462
get : 16978
Sooooooo: почему вставки в std::map
быстрее, чем вставки в std::unordered_map
... В смысле, Ват? std::map
имеет худшую локальность (дерево против массива), необходимо сделать больше распределений (за вставку vs за перестановку + плюс ~1 для каждого столкновения) и, самое главное: имеет другую алгоритмическую сложность(O(logn) vs O (1))!
3 ответов
Я нашел причину: это проблема 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!
Я предполагаю, что вы не правильно определен размер unordered_map
, как Ylisar предложил. Когда цепи растут слишком долго в 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
of 10000000
, и приходилось менять вещи немного для моей версии boost
. Также обратите внимание, что я предварительно определил хэш-таблицу в соответствии SIZE/DEPTH
, где DEPTH
- это оценка длины цепочки ковшей из-за хэш-столкновений.
Edit: Говард указывает мне в комментариях, что коэффициент максимальной нагрузки для unordered_map
is 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");
}
Edit:
Я изменил код, чтобы я мог изменить вон!--14--> более легко.
#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
мой вывод заключается в том, что для любого начального размера хэш-таблицы нет значительной разницы в производительности, кроме как сделать ее равной всему ожидаемому количеству уникальных вставок. Кроме того, я не вижу разницы в производительности на порядок, которую вы наблюдаете.
Я запустил ваш код с помощью 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 он все еще сломан.
[EDIT]
так что да, как кто-то сказал в комментариях, нет причин думать, что производительность GCC 4.9.x будет сопоставим с производительностью VC. Когда у меня будут изменения, я буду тестировать код на ССЗ.
мой ответ - просто установить какую-то базу знаний для других ответов.