Является ли реализация GCC std::unordered map медленной? Если так-то почему?


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

а, std::unordered_map кажется невероятно медленным... Итак, это наш микро-бенчмарк (для параллельной карты мы создали новый поток, чтобы убедиться, что блокировка не оптимизируется и обратите внимание, что я никогда не вставляю 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

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

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 интерфейс, который является стандартным соответствовать и (почти) так же быстро, как googles плотная хэш-карта? Или есть что-то в стандарте, что заставляет исполнителя выбрать неэффективный способ его реализации?

EDIT 2:

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

EDIT 3:

это цифры для std::map:

inserts: 16462
get    : 16978

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

3 95

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, как предложил Илисар. Когда цепи растут слишком долго в 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 - это оценка длины цепочки ковшей из-за хэш-коллизий.

Edit: Говард указывает мне в комментариях, что максимальный коэффициент нагрузки для 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

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

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

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