Я нещодавно в моїй компанії провів орієнтир для різних структур даних, тому відчуваю, що мені потрібно сказати слово. Дуже складно правильно орієнтувати щось.
Бенчмаркінг
В Інтернеті ми рідко знаходимо (якщо взагалі) добре розроблений орієнтир. До сьогодні я знайшов лише орієнтири, які були зроблені журналістським шляхом (досить швидко і підмітаючи десятки змінних під килимом).
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) параметри
Остання проблема полягає в тому, що люди зазвичай тестують на занадто мало варіацій сценарію. На продуктивність контейнера впливають:
- Розподільник
- розмір утримуваного типу
- вартість виконання операції копіювання, операції з присвоєння, операції переміщення, операції побудови, що міститься.
- кількість елементів у контейнері (розмір проблеми)
- тип має тривіальні 3.-операції
- тип ПОД
Точка 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
Введення
Редагувати:
Мої попередні результати включали помилку: вони насправді випробовували впорядковану вставку, яка демонструвала дуже швидку поведінку для плоских карт.
Ці результати я залишив пізніше на цій сторінці, оскільки вони цікаві.
Це правильний тест:
Я перевірив реалізацію, тут немає такого поняття, як відкладений сорт, реалізований на плоских картах. Кожна вставка сортується на льоту, тому цей орієнтир виявляє асимптотичні тенденції:
карта:
хешмапи O (N * log (N)) :
вектор O (N) та плоскі карти: O (N * N)
Попередження : надалі два тести для std::map
і обох flat_map
s баггі і насправді тестується впорядкована вставка (проти випадкової вставки для інших контейнерів. Так, це заплутано вибачте):
Ми бачимо, що впорядкована вставка приводить до натискання на спину, і це надзвичайно швидко. Однак, з не зафіксованих результатів мого еталону, я також можу сказати, що це не поруч з абсолютною оптимальністю для вставки назад. З елементами 10K ідеальна оптимальність зворотного вставки отримується за попередньо зарезервованим вектором. Що дає нам 3 мільйони циклів; ми спостерігаємо тут 4,8 М для впорядкованого введення в flat_map
(отже, 160% від оптимального).
Аналіз: пам’ятайте, що це «випадкова вставка» для вектора, тому масовий 1 мільярд циклів виникає через необхідність зміщення половини (в середньому) даних вгору (один елемент на один елемент) при кожній вставці.
Випадковий пошук 3 елементів (годинник переношений на 1)
в розмірі = 100
в розмірі = 10000
Ітерація
понад розмір 100 (лише тип MediumPod)
понад розмір 10000 (лише тип MediumPod)
Кінцеве зерно солі
Зрештою, я хотів повернутися до "Бенчмаркінг §3 Pt1" (системний розподільник). В недавньому експерименті, який я виконую над розробкою розробленої мною хеш-карти з відкритою адресою , я виміряв розрив у продуктивності понад 3000% між Windows 7 та Windows 8 у деяких std::unordered_map
випадках використання ( обговорюється тут ).
Що змушує мене попередити читача про вищезазначені результати (вони були зроблені на Win7): ваш пробіг може відрізнятися.
з найкращими побажаннями