Является ли gcc std:: unordered_map медленной? Если да - почему?
Мы разрабатываем высокопроизводительное критическое программное обеспечение на С++. Там нам нужна параллельная карта хэша и реализована одна. Итак, мы написали бенчмарк, чтобы выяснить, насколько медленнее наша сопоставимая хэш-карта сравнивается с std::unordered_map
.
Но, std::unordered_map
кажется невероятно медленным... Итак, это наш микро-бенчмарк (для параллельной карты мы породили новый поток, чтобы убедиться, что блокировка не оптимизирована, и обратите внимание, что я никогда не вставляю 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;
(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
должен реализовать более сложный интерфейс. Но я не вижу этого аргумента: мы используем метод bucket в нашем concurrent_map, std::unordered_map
также использует метод bucket-подхода (google::dense_hash_map
не работает, но std::unordered_map
должен быть как минимум так же быстро, как и наша поддержка concurrency -безопасная версия?). Кроме того, я ничего не вижу в интерфейсе, который заставляет функцию, которая делает хэш-карту плохо работать...
Итак, мой вопрос: верно ли, что std::unordered_map
кажется очень медленным? Если нет: что не так? Если да: в чем причина этого.
И мой главный вопрос: зачем вставлять значение в std::unordered_map
настолько ужасно дорого (даже если мы оставляем достаточно места в начале, оно не работает намного лучше - так что перезагрузка, похоже, не является проблемой)?
EDIT:
Прежде всего: да представленный тест не безупречен - это потому, что мы много играли с ним, и это просто взломать (например, дистрибутив uint64
для генерации ints на практике не будет хорошей идеей, исключить 0 в цикле - это глупо и т.д.).
В настоящий момент большинство комментариев объясняют, что я могу сделать unordered_map быстрее, предварительно выделив для этого достаточно места. В нашем приложении это просто невозможно: мы разрабатываем систему управления базами данных и нуждаемся в хэш-карте для хранения некоторых данных во время транзакции (например, информации о блокировке). Таким образом, эта карта может быть всем: от 1 (пользователь просто делает одну вставку и фиксирует) до миллиардов записей (если выполняется полное сканирование таблицы). Здесь просто невозможно предустановить достаточное пространство (и просто выделить много в начале будет потреблять слишком много памяти).
Кроме того, я прошу прощения, что я недостаточно четко сформулировал свой вопрос: мне не очень интересно делать unordered_map быстро (использование плотной хэш-карты googles отлично подходит для нас), я просто не понимаю, где эта огромная производительность различия возникают. Это не может быть просто preallocation (даже с достаточной предопределенной памятью, плотная карта на порядок быстрее, чем unordered_map, наша поддерживаемая рука параллельная карта начинается с массива размером 64 - так меньше, чем unordered_map).
Итак, в чем причина этой плохой производительности std::unordered_map
? Или по-другому спросил: Можно ли написать реализацию интерфейса std::unordered_map
, который является стандартным, совместимым и (почти) так же быстро, как гугл плотная хэш-карта? Или есть что-то в стандарте, которое принуждает исполнителя реализовать неэффективный способ его реализации?
ИЗМЕНИТЬ 2:
Посредством профилирования я вижу, что для целых divions используется много времени. std::unordered_map
использует простые числа для размера массива, в то время как в других реализациях используются полномочия из двух. Почему std::unordered_map
использует простые числа? Лучше работать, если хэш плохой? Для хороших хэшей он делает imho не имеет значения.
ИЗМЕНИТЬ 3:
Это числа для std::map
:
inserts: 16462
get : 16978
Sooooooo: почему вставки в std::map
быстрее, чем вставки в std::unordered_map
... Я имею в виду WAT? std::map
имеет худшую локальность (дерево против массива), необходимо сделать больше распределений (для каждой вставки для каждого рейха + плюс ~ 1 для каждого столкновения) и, что наиболее важно: имеет еще одну алгоритмическую сложность (O (logn) vs O (1 ))
Ответы
Ответ 1
Я нашел причину: это проблема 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!
Ответ 2
Я предполагаю, что вы не правильно определили свой 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");
}
Edit:
Я изменил код, чтобы я мог легче изменить 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
Мой вывод состоит в том, что не существует значительной разницы в производительности для любого начального размера хэш-таблицы, отличного от того, чтобы он был равен всему ожидаемому количеству уникальных вставок. Кроме того, я не вижу порядка разницы в производительности, которую вы наблюдаете.
Ответ 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 он все еще сломан.
[EDIT]
Итак, как кто-то сказал в комментариях, нет оснований полагать, что производительность GCC 4.9.x будет сопоставима с производительностью VC.
Когда у меня будет изменение, я буду тестировать код на GCC.
Мой ответ - просто установить какую-то базу знаний для других ответов.