Является ли реализация 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. Когда у меня будут изменения, я буду тестировать код на ССЗ.

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