Чи повільна реалізація gcc std :: unordered_map? Якщо так - чому?


100

Ми розробляємо високоефективне програмне забезпечення на C ++. Там нам потрібна паралельна хеш-карта та реалізована. Тому ми написали орієнтир, щоб визначити, наскільки повільніше порівняно з нашою паралельною хеш-картою 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потрібно реалізувати більш складний інтерфейс. Але я не можу бачити цього аргументу: ми використовуємо підхід до ковзання у нашій concurrent_map, також std::unordered_mapвикористовуємо підхід до відра ( google::dense_hash_mapчи не так, але чим std::unordered_mapслід бути принаймні швидшим, ніж версія, захищена від конкуренції, захищена рукою?). Крім того, я не бачу нічого в інтерфейсі, що змушує функцію, яка змушує хеш-карту працювати погано ...

Тож моє запитання: чи правда це std::unordered_mapздається дуже повільним? Якщо ні: що не так? Якщо так: яка причина цього.

І моє головне питання: чому вставити значення в std::unordered_mapнастільки страшне дороге (навіть якщо ми зарезервуємо достатньо місця на початку, воно не працює набагато краще - тому повторна перевірка, здається, не є проблемою)?

Редагувати:

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

На даний момент більшість коментарів пояснює, що я можу зробити не упорядкований_мапу швидше, попередньо виділивши достатньо місця для цього. У нашому додатку це просто неможливо: ми розробляємо систему управління базами даних і потребуємо хеш-карту для зберігання деяких даних під час транзакції (наприклад, блокування інформації). Таким чином, ця карта може бути від 1 (користувач робить лише одну вставку і виконує) до мільярдів записів (якщо відбувається сканування повної таблиці). Тут просто неможливо виділити достатньо місця (і просто виділити багато на початку буде споживати занадто багато пам’яті).

Крім того, прошу вибачення, що я не вказав своє питання досить чітким: я не дуже зацікавлений в тому, щоб зробити не упорядкований_мап швидким (використання густої хеш-карти Google google працює добре для нас), я просто не розумію, звідки беруться такі величезні відмінності в продуктивності . Це не може бути просто попереднім розміщенням (навіть при достатній попередньо розподіленій пам’яті щільна карта на порядок швидше, ніж не упорядкована_мапа, наша рука, що підтримується одночасно, починається з масиву розміром 64 - таким чином меншим, ніж не упорядкованим_мапом).

То в чому причина цієї поганої роботи std::unordered_map? Або по-іншому запитують: чи можна написати реалізацію std::unordered_mapінтерфейсу, який є стандартним і відповідає (майже) так само швидко, як густа хеш-карта Google? Або є щось у стандарті, що змушує виконавця обрати неефективний спосіб його реалізації?

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) проти O (1))!


1
Більшість контейнерів у std ДУЖЕ консервативні зі своїми оцінками, я б ознайомився з кількістю відра, яку ви використовуєте (вказано в конструкторі), і збільшити її до кращої оцінки для вашої SIZE.
Ілісар

Ви спробували concurrent_hash_map від Intel TBB? threadingbuildingblocks.org/docs/help/reference/…
MadScientist

1
@MadScientist Ми вважали туберкульоз. Проблема полягає в ліцензуванні: це дослідницький проект, і ми ще не впевнені, як ми його опублікуємо (напевне, з відкритим кодом - але якщо ми хочемо дозволити використання в комерційному продукті, 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, що б це спричинило?
Марк Канлас

30
У списку розсилки вже є звіт. Дискусія, схоже, вказує на "виправлення" max_load_factorповодження, що призвело до різниці у продуктивності.
jxh

Погані терміни для цієї помилки! Я отримував дуже низьку продуктивність з unororder_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є оцінка довжини ланцюга ковша внаслідок хеш-зіткнень.

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");
}

Редагувати:

Я змінив код, щоб я міг змінити його 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

Мій висновок полягає в тому, що для будь-якого початкового розміру хеш-таблиці немає великої різниці в продуктивності, окрім того, що вона дорівнює всій очікуваній кількості унікальних вставок. Крім того, я не бачу різниці між показниками ефективності, яку ви спостерігаєте.


6
std::unordered_mapмає за замовчуванням максимальний коефіцієнт завантаження 1. Отже, за винятком початкової кількості відер, ваш DEPTH ігнорується. При бажанні можна map.max_load_factor(DEPTH).
Говард Хінант

@HowardHinnant: Дякую за цю інформацію. Таким чином DEPTH, ігнорується, але він все ще контролює, наскільки часто карта буде переглянута на більшу карту. Відповідь оновлена, і ще раз дякую
jxh

@ user315052 Так, я знаю, що я можу зробити це краще, надавши йому здоровий розмір на початку, але я не можу цього зробити в нашому програмному забезпеченні (це дослідницький проект - СУБД), і там я не можу знати, скільки я вставлю - вона може коливатися від 0 до 1 млрд ...). Але навіть з попередньою заявою він проходить повільніше, ніж наша карта, і набагато повільніше, ніж googles density_map - мені все одно цікаво, що це робить велику різницю.
Маркус Пільман

@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 він все-таки порушений.

[EDIT]

Отже, так, як хтось сказав у коментарях, немає підстав думати, що продуктивність GCC 4.9.x буде порівнянна з роботою VC. Коли я зміню, я тестуватиму код на GCC.

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


"Я не перевіряв код за допомогою GCC, але думаю, що він може бути порівнянний з продуктивністю VC." Цілком необгрунтована претензія, без порівняльної оцінки, порівнянної з попередньою публікацією. Ця "відповідь" не відповідає на питання в жодному сенсі, не кажучи вже про відповідь на питання "чому".
4ae1e1

2
"Я не перевіряв код за допомогою GCC" ... як це вам вдалося придбати та використовувати MinGW, знаючи про нього так мало? MinGW - це по суті ретельний порт GCC.
підкреслити
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.