boost :: flat_map та його ефективність порівняно з картою та unororder_map


103

Загальновідомо в програмуванні, що локальність пам'яті значно покращує продуктивність завдяки хітам кешу. Нещодавно я дізнався про те, boost::flat_mapщо є векторною реалізацією карти. Здається, він не такий популярний, як ваш типовий map/, unordered_mapтому я не зміг знайти порівняння продуктивності. Як вона порівнює та які найкращі випадки використання для цього?

Дякую!


Важливо зазначити, що boost.org/doc/libs/1_70_0/doc/html/boost/container/… стверджує, що випадкове вставлення займає логарифмічний час, маючи на увазі заповнення boost :: flat_map (шляхом вставки n випадкових елементів) займає O (n log n ) час. Це брехня, як видно з графіків у відповіді @ v.oddou нижче: випадкова вставка - це O (n), і n з них займає час O (n ^ 2).
Дон Хетч

@DonHatch Як повідомити про це тут: github.com/boostorg/container/isissue ? (це може дати підрахунок кількості порівнянь, але це дійсно оману, якщо не супроводжується підрахунком кількості ходів)
Марк

Відповіді:


188

Я нещодавно в моїй компанії провів орієнтир для різних структур даних, тому відчуваю, що мені потрібно сказати слово. Дуже складно правильно орієнтувати щось.

Бенчмаркінг

В Інтернеті ми рідко знаходимо (якщо взагалі) добре розроблений орієнтир. До сьогодні я знайшов лише орієнтири, які були зроблені журналістським шляхом (досить швидко і підмітаючи десятки змінних під килимом).

1) Вам потрібно подумати про прогрівання кешу

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

Правда в тому, що в реальному світі це мало сенсу, тому що ваш кеш не буде теплим, і ваша операція, ймовірно, буде викликана лише один раз. Тому вам потрібно орієнтуватись за допомогою RDTSC та часових речей, що викликають їх лише один раз. Intel склала документ, що описує, як використовувати RDTSC (використовуючи інструкцію cpuid для промивання конвеєра, і викликає його щонайменше 3 рази на початку програми для його стабілізації).

2) вимірювання точності RDTSC

Я також рекомендую зробити це:

u64 g_correctionFactor;  // number of clocks to offset after each measurement to remove the overhead of the measurer itself.
u64 g_accuracy;

static u64 const errormeasure = ~((u64)0);

#ifdef _MSC_VER
#pragma intrinsic(__rdtsc)
inline u64 GetRDTSC()
{
    int a[4];
    __cpuid(a, 0x80000000);  // flush OOO instruction pipeline
    return __rdtsc();
}

inline void WarmupRDTSC()
{
    int a[4];
    __cpuid(a, 0x80000000);  // warmup cpuid.
    __cpuid(a, 0x80000000);
    __cpuid(a, 0x80000000);

    // measure the measurer overhead with the measurer (crazy he..)
    u64 minDiff = LLONG_MAX;
    u64 maxDiff = 0;   // this is going to help calculate our PRECISION ERROR MARGIN
    for (int i = 0; i < 80; ++i)
    {
        u64 tick1 = GetRDTSC();
        u64 tick2 = GetRDTSC();
        minDiff = std::min(minDiff, tick2 - tick1);   // make many takes, take the smallest that ever come.
        maxDiff = std::max(maxDiff, tick2 - tick1);
    }
    g_correctionFactor = minDiff;

    printf("Correction factor %llu clocks\n", g_correctionFactor);

    g_accuracy = maxDiff - minDiff;
    printf("Measurement Accuracy (in clocks) : %llu\n", g_accuracy);
}
#endif

Це вимірювач розбіжностей, і він займе мінімум усіх виміряних значень, щоб уникнути періодичного отримання -10 ** 18 (64 біт перших негативних значень).

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

3) параметри

Остання проблема полягає в тому, що люди зазвичай тестують на занадто мало варіацій сценарію. На продуктивність контейнера впливають:

  1. Розподільник
  2. розмір утримуваного типу
  3. вартість виконання операції копіювання, операції з присвоєння, операції переміщення, операції побудови, що міститься.
  4. кількість елементів у контейнері (розмір проблеми)
  5. тип має тривіальні 3.-операції
  6. тип ПОД

Точка 1 важлива, тому що контейнери виділяють час від часу, і це має велике значення, якщо вони розподіляються за допомогою CRT "new" або деякої визначеної користувачем операції, як розподіл пулу, фріліст чи інші ...

( для людей, які цікавляться про pt 1, приєднайтеся до загадкової нитки в gamedev про вплив на продуктивність системного розподільника )

Точка 2 полягає в тому, що деякі контейнери (скажімо, A) втрачають час на копіювання речей, і чим більше тип, тим більший накладні витрати. Проблема полягає в тому, що, порівнюючи з іншим контейнером B, A може перемагати B для малих типів та програвати для більших типів.

Пункт 3 такий же, як і пункт 2, за винятком того, що він помножує вартість на деякий коефіцієнт зважування.

Пункт 4 - це питання великого O, змішаного з питаннями кешу. Деякі контейнери поганий складності можуть в значній мірі перевершують низьку складність контейнерів для невеликої кількості типів (наприклад , mapпроти прогнозу vector, тому що їх кеш місцезнаходження добре, але mapфрагменти пам'яті). І тоді на деякій точці перетину вони втратять, тому що розміщений загальний розмір починає «просочуватися» до основної пам’яті та спричиняти пропуски кешу, плюс те, що асимптотична складність може почати відчуватися.

У пункті 5 йдеться про те, що компілятори зможуть видалити речі, які є порожніми або тривіальними під час компіляції. Це може значно оптимізувати деякі операції, оскільки контейнери шаблоновані, тому кожен тип матиме власний профіль продуктивності.

У пункті 6, як і у пункті 5, ПОД можуть отримати користь від того, що конструкція копії - це лише мемпія, а деякі контейнери можуть мати конкретну реалізацію для цих випадків, використовуючи часткові спеціалізації шаблонів або SFINAE для вибору алгоритмів відповідно до ознак Т.

Про плоску карту

Мабуть, плоска карта є відсортованою векторною обгорткою, як Loki AssocVector, але з деякими додатковими модернізаціями, що надходять із C ++ 11, використовуючи семантику переміщення для прискорення вставки та видалення окремих елементів.

Це все-таки замовлений контейнер. Більшість людей зазвичай не потребують замовляючої частини, тому існування unordered...

Ви думали, що, можливо, вам потрібен flat_unorderedmap? що було б щось подібне google::sparse_mapчи щось подібне - карта хешу з відкритою адресою.

Проблема хеш-карт з відкритою адресою полягає в тому, що під час rehashвони повинні копіювати все навколо на нову розширену плоску землю, тоді як стандартна не упорядкована карта просто повинна відтворити хеш-індекс, тоді як виділені дані залишаються там, де вони є. Недоліком звичайно є те, що пам’ять роздроблена як пекло.

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

Типовим коефіцієнтом навантаження є 0.8; тому вам потрібно подбати про це, якщо ви зможете заздалегідь intended_filling * (1/0.8) + epsilonрозмістити свою хеш-карту перед її заповненням, завжди попередньо розміром до цього : це дасть вам гарантію, що вам більше не доведеться зловмисно повторно переробляти та відновлювати все під час заповнення.

Перевага закритих адресних карт ( std::unordered..) полягає в тому, що вам не потрібно дбати про ці параметри.

Але boost::flat_mapвпорядкований вектор; отже, вона завжди матиме асимптотичну складність журналу (N), що менш добре, ніж хеш-карта з відкритою адресою (амортизований постійний час). Ви також повинні це врахувати.

Результати порівняння

Це тест, що включає різні карти (з intключовими та __int64/ somestructяк значеннями) та std::vector.

інформація про перевірені типи:

typeid=__int64 .  sizeof=8 . ispod=yes
typeid=struct MediumTypePod .  sizeof=184 . ispod=yes

Введення

Редагувати:

Мої попередні результати включали помилку: вони насправді випробовували впорядковану вставку, яка демонструвала дуже швидку поведінку для плоских карт.
Ці результати я залишив пізніше на цій сторінці, оскільки вони цікаві.
Це правильний тест: випадкова вставка 100

випадкова вставка 10000

Я перевірив реалізацію, тут немає такого поняття, як відкладений сорт, реалізований на плоских картах. Кожна вставка сортується на льоту, тому цей орієнтир виявляє асимптотичні тенденції:

карта:
хешмапи O (N * log (N)) :
вектор O (N) та плоскі карти: O (N * N)

Попередження : надалі два тести для std::mapі обох flat_maps баггі і насправді тестується впорядкована вставка (проти випадкової вставки для інших контейнерів. Так, це заплутано вибачте):
змішана вставка з 100 елементів без застереження

Ми бачимо, що впорядкована вставка приводить до натискання на спину, і це надзвичайно швидко. Однак, з не зафіксованих результатів мого еталону, я також можу сказати, що це не поруч з абсолютною оптимальністю для вставки назад. З елементами 10K ідеальна оптимальність зворотного вставки отримується за попередньо зарезервованим вектором. Що дає нам 3 мільйони циклів; ми спостерігаємо тут 4,8 М для впорядкованого введення в flat_map(отже, 160% від оптимального).

змішана вставка з 10000 елементів без застереження Аналіз: пам’ятайте, що це «випадкова вставка» для вектора, тому масовий 1 мільярд циклів виникає через необхідність зміщення половини (в середньому) даних вгору (один елемент на один елемент) при кожній вставці.

Випадковий пошук 3 елементів (годинник переношений на 1)

в розмірі = 100

пошук у вікні контейнера з 100 елементів

в розмірі = 10000

пошук у вікні контейнера з 10000 елементів

Ітерація

понад розмір 100 (лише тип MediumPod)

Ітерація понад 100 середніх стручків

понад розмір 10000 (лише тип MediumPod)

Ітерація понад 10000 середніх стручків

Кінцеве зерно солі

Зрештою, я хотів повернутися до "Бенчмаркінг §3 Pt1" (системний розподільник). В недавньому експерименті, який я виконую над розробкою розробленої мною хеш-карти з відкритою адресою , я виміряв розрив у продуктивності понад 3000% між Windows 7 та Windows 8 у деяких std::unordered_mapвипадках використання ( обговорюється тут ).
Що змушує мене попередити читача про вищезазначені результати (вони були зроблені на Win7): ваш пробіг може відрізнятися.

з найкращими побажаннями


1
о, в цьому випадку це має сенс. Постійні амортизовані гарантії вектора застосовуються лише при вставлянні в кінці. Вставлення у випадкових положеннях повинно становити середнє значення O (n) на вставку, оскільки все після точки вставки потрібно перемістити вперед. Таким чином, ми очікуємо квадратичної поведінки у вашому орієнтирі, який вибухає досить швидко, навіть для невеликих N. Реалізації стилю AssocVector, ймовірно, відкладають сортування, поки не потрібен пошук, наприклад, а не сортування після кожної вставки. Важко сказати, не бачачи свого еталону.
Біллі ONeal

1
@BillyONeal: Ах, ми перевірили код з колегою і виявили винуватця, моє "випадкове" вставлення було замовлено, тому що я використовував std :: set, щоб переконатися, що вставлені ключі унікальні. Це звичайна імбецильність, але я вирішив, що за допомогою random_shuffle я зараз відновлюю роботу, і незабаром з’являться нові результати. Тож тест в його нинішньому стані доводить, що "впорядкована вставка" проклята швидко.
v.oddou

3
"Intel має папір" ← і ось це
ізоморфізми

5
Можливо, я пропускаю щось очевидне, але я не розумію, чому випадковий пошук уповільнений flat_mapпорівняно з std::map- чи хтось може пояснити цей результат?
хлопець

1
Я б пояснив це як специфічні накладні витрати на впровадження цього часу, а не як внутрішній характер flat_mapконтейнера. Тому що Aska::версія швидша, ніж std::mapпошук. Доведення того, що є місце для оптимізації. Очікувана продуктивність асимптотично однакова, але, можливо, трохи краща завдяки локалізації кешу. З наборами великих розмірів вони повинні сходитися.
v.oddou

6

З документів виходить, що це аналог тому, з Loki::AssocVectorяким я досить важкий користувач. Оскільки він заснований на векторі, він має характеристики вектора, тобто:

  • Ітератори втрачають силу, коли sizeзростають capacity.
  • Коли вона зростає за межі, capacityїй потрібно перерозподіляти та переміщувати об'єкти, тобто вставлення не гарантується постійним часом, за винятком спеціального випадку вставки, endколиcapacity > size
  • Пошук швидше, ніж std::mapзавдяки локалізації кешу, двійкового пошуку, який має ті ж характеристики продуктивності, що std::mapінакше
  • Використовує менше пам'яті, оскільки це не пов'язане бінарне дерево
  • Він ніколи не скорочується, якщо ви не примусово скажете це (оскільки це запускає перерозподіл)

Найкраще використовувати, коли ви знаєте кількість елементів заздалегідь (тому ви можете reserveнаперед) або коли вставлення / видалення рідкісне, але пошук часто. Інвалідація ітератора робить його дещо громіздким у деяких випадках використання, тому вони не є взаємозамінними з точки зору коректності програми.


1
false :) вимірювання, наведені вище, показують, що для операцій з пошуку карта швидша, ніж flat_map, я думаю, що для збільшення ppl потрібно виправити реалізацію, але теоретично ви праві.
NoSenseEtAl
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.