Важливе попереднє читання: Microarch pdf Agner Fog , а також, можливо, також те, що повинен знати кожен програміст про пам'ять . Дивіться також інші посилання вx86тегі-вікі, особливо посібники з оптимізації Intel, і аналіз Девід Кантера з мікроархітектури Haswell з діаграмами .
Дуже круте завдання; набагато краще, ніж ті, що я бачив, де студентів просили оптимізувати якийсь кодgcc -O0
, вивчаючи купу хитрощів, які не мають значення в реальному коді. У цьому випадку вас просять дізнатись про конвеєр процесора і використовувати його для керування вашими зусиллями з оптимізації, а не просто сліпими здогадами. Найцікавішою частиною цього є виправдання кожної песимізації "диявольською некомпетентністю", а не навмисною злобою.
Проблеми з формулюванням та кодом призначення :
Спеціальні параметри цього коду обмежені. Він не використовує жодних масивів, і значна частина витрат становить дзвінки в exp
/ log
бібліотечні функції. Існує не очевидний спосіб мати більш-менш паралелізм на рівні інструкцій, і ланцюг залежності, що переноситься циклом, дуже короткий.
Я хотів би побачити відповідь, що намагалася отримати уповільнення від перестановки виразів для зміни залежностей, зменшення ILP просто від залежностей (небезпек). Я цього не робив.
Процесори сімейства Intel Sandybridge - це агресивні нестандартні конструкції, які витрачають багато транзисторів та потужність, щоб знайти паралелізм та уникнути небезпек (залежностей), які б заважали класичному трубопроводу RISC в порядку . Зазвичай єдиними традиційними небезпеками, які сповільнюють це, є "справжні" залежності RAW, які призводять до обмеження пропускної здатності затримкою.
Небезпеки для WAR та WAW для реєстрів - це не велика проблема, завдяки перейменуванню реєстру . (за виняткомpopcnt
//lzcnt
/tzcnt
, які мають помилкову залежність від свого призначення від процесорів Intel , навіть якщо це лише для запису. тобто WAW обробляється як небезпека RAW + запис). Для впорядкування пам'яті сучасні процесори використовують черги на зберігання, щоб затримати фіксацію в кеш до виходу на пенсію, також уникаючи небезпек WAR та WAW .
Чому мульс займає лише 3 цикли на Haswell, відмінні від таблиць інструкцій Agner? детальніше про перейменування реєстру та приховування затримок FMA у циклі продуктових точок FP.
Бренд "i7" був представлений разом з Nehalem (спадкоємцем Core2) , а деякі посібники від Intel навіть кажуть "Core i7", коли, здається, означають Nehalem, але вони зберігали бренди "i7" для Sandybridge та пізніших мікроархітектур. SnB - це коли сім'я P6 перетворилася на новий вид, сімейство SnB . Багато в чому Nehalem має більше спільного з Pentium III, ніж із Sandybridge (наприклад, реєструвати збитки для читання та стійкі для читання ROB не трапляються на SnB, оскільки він змінився на використання файлу фізичного регістра. Також загальний кеш і інший внутрішній формат взагалі). Термін "архітектура i7" не корисний, тому що мало сенсу групувати сім'ю SnB з Nehalem, але не з Core2. (Хоча Nehalem ввів спільну архітектуру кешу L3 для з'єднання декількох ядер разом. А також інтегровані графічні процесори. Отже, на рівні чіпа, іменування має більше сенсу.)
Короткий зміст хороших ідей, якими може виправдатись дьявольська некомпетентність
Навіть діаболічно некомпетентні навряд чи додадуть очевидно марну роботу або нескінченний цикл, і заплутатися з класами C ++ / Boost виходить за рамки завдання.
- Багатопотокові з одним лічильником спільного
std::atomic<uint64_t>
циклу, тому правильна загальна кількість ітерацій відбувається. Атомний uint64_t особливо поганий -m32 -march=i586
. Для отримання бонусних балів слід домогтися її вирівнювання та перетину меж сторінки з нерівномірним розділенням (не 4: 4).
- Неправдивий спільний доступ для деяких інших атомних змінних -> помилка замовлення пам’яті трубопроводу очищає, а також додаткові пропуски кешу.
- Замість використання
-
на змінних FP, XOR високий байт з 0x80, щоб перевернути біт знаків, викликаючи стійло переадресації магазину .
- Визначте кожну ітерацію самостійно, щось ще важче, ніж
RDTSC
. наприклад, CPUID
/ RDTSC
або функція часу, яка здійснює системний виклик. Інструкції щодо серіалізації за своєю суттю не підходять для роботи.
- Змініть множення на константи на ділення на їх зворотні ("для зручності читання"). div повільний і не повністю конвеєрний.
- Векторизувати множення / sqrt за допомогою AVX (SIMD), але не використовувати його
vzeroupper
перед викликами до скалярної математичної бібліотеки exp()
та log()
функцій, викликаючи AVX <-> SSE перехідні стійли .
- Зберігайте висновок RNG у пов'язаному списку або в масивах, які ви перебуваєте поза порядком. Те саме для результату кожної ітерації та підсумок в кінці.
Також висвітлюється у цій відповіді, але виключається із резюме: пропозиції, які будуть настільки ж повільними для непрохідного процесора, або які не здаються виправданими навіть при дьявольській некомпетентності. наприклад, багато ідей компілятора gimp, які створюють очевидно інший / гірший ASM.
Багатопотокові погано
Можливо, використовуйте OpenMP для багатопотокових циклів з дуже малою кількістю ітерацій, з набагато більшими накладними, ніж збільшення швидкості. Ваш код Монте-Карло має достатній паралелізм, щоб насправді отримати швидкість, хоча, особливо. якщо нам вдасться зробити кожну ітерацію повільною. (Кожна нитка обчислює часткове payoff_sum
, додане в кінці). #omp parallel
на цьому циклі, ймовірно, буде оптимізація, а не песимізація.
Багатопотокові, але змушуйте обидва потоки ділити один і той же лічильник циклу (з atomic
кроком, щоб загальна кількість ітерацій була правильною). Це здається диявольськи логічним. Це означає використовувати static
змінну як лічильник циклу. Це виправдовує використання atomic
для лічильників циклів і створює фактичну кеширувальну лінію ping-ponging (до тих пір, поки потоки не працюватимуть на одному фізичному ядрі з гіперточенням; це може бути не так повільно). У будь-якому випадку це набагато повільніше, ніж випадки, що не суперечать lock inc
. А lock cmpxchg8b
щоб атомний приріст, який претендує uint64_t
на 32-бітну систему, доведеться повторити спробу в циклі замість того, щоб апаратний арбітраж атомного inc
.
Також створіть помилковий обмін , де кілька потоків зберігають свої приватні дані (наприклад, стан RNG) у різних байтах однієї лінії кешу. (Навчальний посібник Intel про це, включаючи лічильники парфу для перегляду) . У цьому є специфічний для мікроархітектури аспект : процесори Intel спекулюють на тому, що неправильне впорядкування пам’яті не відбувається, і для того, щоб виявити це, принаймні на P4 , існує порядок оперативної пам’яті в порядку пам'яті . Штраф може бути не таким великим для Haswell. Як вказує це посилання, lock
редакція редактора передбачає, що це станеться, уникаючи помилок. Звичайне навантаження припускає, що інші ядра не можуть визнати недійсним рядок кешу між тим, коли завантаження виконується, і коли воно скасовується в програмному порядку (якщо ви не використовуєтеpause
). Справжній спільний доступ без lock
інструкцій редагування зазвичай є помилкою. Було б цікаво порівняти неатомний лічильник спільного циклу з атомним корпусом. Щоб дійсно песимізувати, збережіть лічильник загального атомного циклу та викликайте помилковий обмін у тій же чи іншій лінії кешу для якоїсь іншої змінної.
Випадкові ідеї, характерні для урха:
Якщо ви можете ввести будь-які непередбачувані гілки , це істотно песимізує код. Сучасні процесори x86 мають досить довгі конвеєри, тому помилковий прогноз коштує ~ 15 циклів (при запуску із загального кешу).
Ланцюги залежностей:
Я думаю, це було однією із запланованих частин завдання.
Поразка здатності процесора використовувати паралелізм рівня інструкцій, вибираючи порядок операцій, що має один довгий ланцюг залежностей замість кількох коротких ланцюгів залежностей. Компіляторам заборонено змінювати порядок операцій для обчислень ПП, якщо ви не використовуєте -ffast-math
, оскільки це може змінити результати (як обговорюється нижче).
Щоб дійсно зробити це ефективним, збільште довжину ланцюга залежності, що переноситься циклом. Ніщо не вискакує як очевидне: Написані петлі мають дуже короткі ланцюги залежностей, що переносяться циклом: лише FP-додаток. (3 цикли). Багаторазові ітерації можуть мати обчислення під час польоту одразу, оскільки вони можуть розпочатися задовго до payoff_sum +=
кінця попередньої ітерації. ( log()
і exp
візьміть багато інструкцій, але не набагато більше, ніж вікно поза замовлення Haswell для пошуку паралелізму: ROB size = 192 Uops з конденсованим доменом, і розмір планувальника = 60 Uops з конденсованим доменом. Як тільки виконання поточної ітерації прогресує досить далеко, щоб звільнити місце для вказівок щодо наступної ітерації щодо випуску, будь-які її частини, які мають готові входи (тобто незалежний / окремий ланцюг dep), можуть почати виконання, коли старші інструкції залишають одиниці виконання безкоштовно (наприклад, через те, що вони обмежені затримкою, а не пропускною здатністю.)
Стан RNG майже напевно буде більш тривалим циклом залежності, ніж цей addps
.
Використовуйте більш повільні / більш операції з ПП (особливо підрозділ):
Ділимо на 2.0, а не множимо на 0,5 тощо. Мультиплікаційний потенціал є сильно конвеєрним в конструкціях Intel і має пропускну здатність на 0,5 с на Haswell і пізніше. FP divsd
/ divpd
є лише частково конвеєрним . (Хоча Skylake має вражаючу пропускну здатність на 4c для divpd xmm
, із затримкою 13-14c, проти Нехалема зовсім (7-22c)).
Це do { ...; euclid_sq = x*x + y*y; } while (euclid_sq >= 1.0);
чітко тестує відстань, так що явно це було б належним sqrt()
чином. : P ( sqrt
навіть повільніше, ніж div
).
Як пропонує @Paul Clayton, переписання виразів з асоціативними / розподільними еквівалентами може ввести більше роботи (доки ви не використовуєте, -ffast-math
щоб дозволити компілятору повторно оптимізувати). (exp(T*(r-0.5*v*v))
міг стати exp(T*r - T*v*v/2.0)
. Зауважте, що математика на реальних числах асоціативна, математика з плаваючою комою не є , навіть не враховуючи переповнення / NaN (через що -ffast-math
за замовчуванням не ввімкнено). Дивіться коментар Павла щодо дуже волохатої вкладеної pow()
пропозиції.
Якщо ви можете масштабувати обчислення до дуже малих чисел, то FP математика може взяти ~ 120 додаткових циклів, щоб потрапити в мікрокод, коли операція з двома нормальними числами дає денормальне . Точні номери та деталі див. У pdf-файлі Microarch pg Agner Fog. Це малоймовірно, оскільки у вас багато примножень, тому масштабний коефіцієнт буде розміщений у квадраті та переповнено до 0,0. Я не бачу жодного способу виправдати необхідне масштабування некомпетентністю (навіть диявольською), лише навмисною злобою.
Якщо ви можете використовувати внутрішні слова ( <immintrin.h>
)
Використовуйте movnti
для виселення своїх даних із кешу . Діаболічний: це новий і слабо упорядкований, так що це дозволить процесору запустити його швидше, правда? Або подивіться це пов'язане запитання на випадок, коли комусь загрожує зробити саме це (бо розкидані записи, де лише деякі місця були гарячими). clflush
мабуть, неможливо без злоби.
Використовуйте цілі перемішання між математичними операціями FP, щоб викликати затримки в обході.
Змішування інструкцій SSE та AVX без належного використання vzeroupper
спричиняє великі стійки в попередньому Skylake (і різний штраф у Skylake ). Навіть без цього векторизація погано може бути гіршою, ніж скалярна (більше циклів витрачає переміщення даних у / з векторів, ніж збережене, виконуючи операції add / sub / mul / div / sqrt одночасно для 4 ітерацій Монте-Карло, з векторами 256b) . Блоки виконання add / sub / mul є повністю конвеєрними та на повну ширину, але div та sqrt на векторах 256b не такі швидкі, як на векторах 128b (або скалярах), тому швидкість роботи не є драматичноюdouble
.
exp()
і log()
не мають апаратної підтримки, щоб ця частина вимагала вилучення векторних елементів назад до скалярного і виклику функції бібліотеки окремо, а потім переміщення результатів назад у вектор. Як правило, libm компілюється лише для використання SSE2, тому використовуватиме застарілі SSE-кодування скалярних інструкцій з математики. Якщо ваш код використовує 256b векторів і дзвінки, exp
не роблячи vzeroupper
спочатку, то ви зупиняєтесь. Після повернення інструкція AVX-128, як vmovsd
налаштувати наступний векторний елемент як аргумент для exp
, також зупиниться. А потім exp()
знову зупиниться, коли він виконує інструкцію SSE. Саме це сталося в цьому питанні , спричинивши 10-кратне уповільнення. (Дякую @ZBoson).
Дивіться також експерименти Натана Курца з математичною lib Intel проти glibc для цього коду . Майбутній glibc поставиться з векторизованими реалізаціями exp()
тощо.
Якщо націлено на попередній показник IvB або esp. Негалем, спробуйте отримати gcc, щоб викликати стійкі часткові регістри з операціями 16 біт або 8 біт з наступними операціями з 32 або 64 бітами. У більшості випадків gcc буде використовуватися movzx
після 8 або 16-бітної операції, але ось випадок, коли gcc змінюється, ah
а потім читаєтьсяax
З (вбудованим) посиланням:
За допомогою (вбудованого) ASM ви можете зламати кеш взагалі: 32B фрагмент коду, який не вміщується у трьох лініях кешу 6uop, змушує переключитися з кеша взагалі на декодери. Некомпетентність, що ALIGN
використовує багато однобайтових nop
s замість пари довгих nop
s на цілі гілки всередині внутрішнього циклу, може зробити цю справу. Або покладіть накладку вирівнювання після мітки, а не перед. : P Це має значення лише в тому випадку, якщо передній край - це вузьке місце, якого не буде, якщо нам вдалося песимізувати решту коду.
Використовуйте самомодифікуючий код, щоб запустити очищення трубопроводу (він же машинних ядер).
LCP кіоски з 16-бітових інструкцій із занадто великими безпосередніми розмірами, щоб вміститись у 8 біт, навряд чи будуть корисними. Загальний кеш на SnB і пізніше означає, що ви сплачуєте штраф декодування лише один раз. У Nehalem (перший i7) він може працювати для циклу, який не вписується в буфер циклу 28 взагалі. gcc інколи буде генерувати такі інструкції, навіть -mtune=intel
коли і коли він міг би використовувати 32-бітну інструкцію.
Поширена фразеологізація часу - це CPUID
(серіалізувати) тодіRDTSC
. Час кожної ітерації окремо з CPUID
/ , RDTSC
щоб переконатися , RDTSC
чи не замовити з попередніми інструкціями, які будуть сповільнювати речі вниз багато . (У реальному житті розумним способом часу є підключення всіх ітерацій разом, а не приурочення кожного окремо та додавання їх).
Причиняється багато пропусків кешу та інших сповільнень пам'яті
Використовуйте union { double d; char a[8]; }
для деяких своїх змінних. Викликайте стійло переадресації магазину , виконавши вузький магазин (або Read-Modify-Write) лише до одного з байтів. (Ця стаття у вікі також охоплює багато інших мікроархітектурних матеріалів для черг на завантаження / зберігання). наприклад, переверніть знак double
використання XOR 0x80 лише на високому байті , а не на -
операторі. Діаболічно некомпетентний розробник, можливо, почув, що FP повільніше, ніж ціле число, і, таким чином, спробуйте зробити якомога більше, використовуючи цілі ops. (Дуже хороший компілятор, орієнтований на математику FP в регістрах SSE, можливо, може скласти це доxorps
з константою в іншому регістрі xmm, але єдиний спосіб, який не є страшним для x87, це якщо компілятор зрозуміє, що це заперечує значення і замінює наступне додавання на віднімання.)
Використовуйте, volatile
якщо ви компілюєте -O3
та не використовуєте std::atomic
, щоб змусити компілятор фактично зберігати / перезавантажувати всюди. Глобальні змінні (замість локальних) також змусять деякі магазини / перезавантаження, але слабке впорядкування моделі пам’яті C ++ не вимагає, щоб компілятор весь час розливався / перезавантажувався в пам'ять.
Замініть місцеві параметри членами великої структури, щоб ви могли керувати компонуванням пам'яті.
Використовуйте масиви в структурі для заміщення (і зберігання випадкових чисел для обгрунтування їх існування).
Виберіть макет пам’яті, щоб все перейшло в інший рядок у тому ж «наборі» в кеші L1 . Це лише восьмисторонній асоціативний характер, тобто кожен набір має 8 "способів". Лінії кеш-пам'яті - 64B.
Ще краще, розмістіть речі рівно на 4096B, оскільки навантаження мають помилкову залежність від магазинів на різних сторінках, але з однаковим зміщенням у межах сторінки . Агресивні процесори, що вийшли з ладу, використовують розбір пам’яті, щоб визначити, коли навантаження та сховища можуть бути впорядковані без зміни результатів , а реалізація Intel має помилкові позитиви, які не дозволяють завантажувати навантаження рано. Ймовірно, вони перевіряють лише біти нижче зміщення сторінки, тому перевірка може розпочатися до того, як TLB переклав високі біти з віртуальної сторінки на фізичну. Як і посібник Агнера, дивіться відповідь Стівена Канона , а також розділ наприкінці відповіді @Krazy Glew на те саме питання. (Енді Глі був одним з архітекторів Intel-оригінальної мікроархітектури P6.)
Використовуйте, __attribute__((packed))
щоб дозволити неправильно вирівняти змінні, щоб вони охоплювали кеш-лінію або навіть межі сторінки. (Отже, для завантаження double
потрібні дані з двох кеш-рядків). Нерівні навантаження не мають штрафу в жодному процесорі Intel i7, крім випадків, коли перетинає рядки кешу та рядки сторінок. Розщеплення кеш-лінії все ще займає додаткові цикли . Skylake різко знижує штраф за розділене завантаження сторінки, зі 100 до 5 циклів. (Розділ 2.1.3) . Можливо, це стосується можливості паралельних двох прогулянок на сторінці.
Розбиття сторінки на аркуші atomic<uint64_t>
має бути приблизно в гіршому випадку , особливо. якщо це 5 байт на одній сторінці і 3 байти на іншій сторінці, або що-небудь інше, ніж 4: 4. Навіть розщеплення в середині є більш ефективними для розщеплення кеш-ліній з 16В векторами на деяких урах, IIRC. Покладіть усе в alignas(4096) struct __attribute((packed))
(звичайно, щоб заощадити місце), включаючи масив для зберігання результатів RNG. Досягніть нерівності, використовуючи uint8_t
чи що- uint16_t
небудь перед лічильником.
Якщо ви можете змусити компілятор використовувати індексовані режими адресації, це переможе взагалі мікро-синтез . Можливо, використовуючи #define
s для заміни простих скалярних змінних my_data[constant]
.
Якщо ви можете ввести додатковий рівень непрямості, тому адреси завантаження / зберігання невідомі рано, що може ще більше песимізувати.
Поперечні масиви в безперервному порядку
Я думаю, що ми можемо придумати некомпетентне обґрунтування введення масиву в першу чергу: Це дозволяє нам відокремити генерацію випадкових чисел від використання випадкових чисел. Результати кожної ітерації також можна зберігати в масиві, який підсумовується пізніше (з більшою діаболічною некомпетентністю).
Для "максимальної випадковості" ми могли б мати нитку, що перекидається через випадковий масив, записуючи в нього нові випадкові числа. Потік, що споживає випадкові числа, може генерувати випадковий індекс для завантаження випадкового числа. (Тут є певна робота, але мікроархітектурно це допомагає дізнатися адреси завантаження рано, тому будь-які можливі затримки завантаження можуть бути вирішені до необхідності завантаження даних.) Наявність читача та записувача на різних ядрах спричинить помилку впорядкування пам'яті - конвеєрний конвеєр очищається (як обговорювалося раніше у випадку обміну помилковими помилками).
Для максимальної песимізації переведіть цикл на свій масив з кроком 4096 байт (тобто 512 пар). напр
for (int i=0 ; i<512; i++)
for (int j=i ; j<UPPER_BOUND ; j+=512)
monte_carlo_step(rng_array[j]);
Отже шаблон доступу дорівнює 0, 4096, 8192, ...,
8, 4104, 8200, ...
16, 4112, 8208, ...
Це те, що ви отримаєте для доступу до 2D масиву, як double rng_array[MAX_ROWS][512]
у неправильному порядку (перекидання рядків замість стовпців у рядку у внутрішньому циклі, як запропонував @JesperJuhl). Якщо діаболічна некомпетентність може виправдати двовимірний масив з такими розмірами, то некомпетентність у реальному масштабі саду легко виправдовує циклічність із неправильним шаблоном доступу. Це відбувається в реальному коді в реальному житті.
За необхідності відрегулюйте межі циклу, щоб використовувати багато різних сторінок замість того, щоб використовувати кілька і тих же сторінок, якщо масив не такий великий. Попереднє завантаження обладнання не працює (як і взагалі) на сторінках. Попередній програвач може відстежувати один потік вперед та один зворотний потік на кожній сторінці (що тут відбувається), але діятиме на нього лише в тому випадку, якщо пропускна здатність пам’яті вже не насичена без попереднього вибору.
Це також призведе до безлічі пропусків TLB, якщо сторінки не об'єднаються у величезну сторінку ( Linux робить це умовно-malloc
new
mmap(MAP_ANONYMOUS)
методично для анонімних (не підтримуваних файлів) виділень, таких як / що використовують ).
Замість масиву для зберігання списку результатів можна використовувати зв'язаний список . Тоді кожна ітерація потребує навантаження, що переслідує покажчик (справжня небезпека залежності RAW для адреси навантаження наступного навантаження). З поганим розподільником, вам може вдатися розкидати вузли списку навколо пам’яті, перемігши кеш. Діаболічно некомпетентний розподільник може розмістити кожен вузол на початку своєї власної сторінки. (наприклад, розподіляти mmap(MAP_ANONYMOUS)
безпосередньо, не розбиваючи сторінки та відстежуючи розміри об'єктів, щоб належним чином підтримувати free
).
Вони насправді не стосуються мікроархітектури і мають мало спільного з конвеєром (більшість з них також буде уповільненням роботи непрохідного процесора).
Дещо поза темою: змусити компілятор генерувати гірший код / зробити більше роботи:
Використовуйте C ++ 11 std::atomic<int>
і std::atomic<double>
для найбільш песимального коду. Інструкції MFENCE та lock
ed є досить повільними, навіть не маючи суперечок з іншого потоку.
-m32
зробить повільніше код, тому що x87 код буде гірше, ніж код SSE2. 32-бітова конвенція на основі стека вимагає більше інструкцій і передає навіть FP-аргументи на стеку таким функціям exp()
. atomic<uint64_t>::operator++
для -m32
вимагає lock cmpxchg8B
циклу (i586). (Тож використовуйте це для лічильників циклу! [Злий сміх]).
-march=i386
буде також песимізувати (дякую @Jesper). FP порівняно з fcom
повільнішими, ніж 686 fcomi
. Pre-586 не забезпечує атомний 64-бітовий сховище (не кажучи вже про cmpxchg), тому всі 64-бітові atomic
операційні системи компілюються для викликів функції libgcc (що, ймовірно, компілюється для i686, а не фактично використовуючи замок). Спробуйте це за посиланням Godbolt Compiler Explorer в останньому абзаці.
Використовуйте long double
/ sqrtl
/ expl
для додаткової точності та додаткової повільності в ABI, де sizeof ( long double
) становить 10 або 16 (з накладкою для вирівнювання). (IIRC, 64 - розрядні Windows , використовує 8byte long double
еквівалент double
. ( У всякому разі, навантаження / магазин 10byte (80bit) FP операндами 4/7 микрооперации, VS. float
або double
тільки з 1 моп кожен для fld m64/m32
/ fst
). Примус x87 з long double
ураженнями автоматичної векторизації навіть для gcc -m64 -march=haswell -O3
.
Якщо не використовуються atomic<uint64_t>
лічильники циклів, використовуйте long double
для всього, включаючи лічильники циклів.
atomic<double>
компілює, але операції читання-зміни-запису на зразок +=
не підтримуються для нього (навіть на 64-бітній). atomic<long double>
має викликати функцію бібліотеки лише для атомних навантажень / сховищ. Це, мабуть, дійсно неефективно, тому що x86 ISA, природно, не підтримує атомні 10-байтові навантаження / сховища , і єдиний спосіб, про який я можу подумати, не блокуючи ( cmpxchg16b
), вимагає 64-бітового режиму.
У випадку -O0
, розбиття великого вираження шляхом присвоєння деталей тимчасовим варам, спричинить більше зберігання / перезавантаження. Без цього volatile
чи іншого, це не має значення з налаштуваннями оптимізації, які використовували б реальну збірку реального коду.
Правила псевдонімування дозволяють створювати char
псевдонім будь-що, тому зберігання через char*
сили компілятора зберігає / перезавантажує все до / після сховища байтів, навіть у -O3
. (Це проблема для автоматичного векторизації коду, який працює, наприклад, на масивіuint8_t
.)
Спробуйте uint16_t
лічильники циклів, щоб змусити усікання до 16 біт, ймовірно, використовуючи 16-бітовий розмір операнду (потенційні стійла) та / або додаткові movzx
інструкції (безпечно). Переповнення підпису є невизначеною поведінкою , тому, якщо ви не використовуєте -fwrapv
або принаймні -fno-strict-overflow
, підписані циклічні лічильники не повинні повторно підписуватись за кожну ітерацію , навіть якщо вони використовуються як компенсації до 64-бітових покажчиків.
float
Знову примусово перетворіть з цілого числа в та назад. Та / або double
<=> float
перетворення. Інструкції мають більшу затримку, а скалярний int-> float ( cvtsi2ss
) погано розроблений, щоб не нулювати решту регістра xmm. ( pxor
з цієї причини gcc вставляє додатковий, щоб зламати залежності.)
Часто встановлюйте спорідненість CPU до іншого процесора (запропоновано @Egwor). дьявольські міркування: Ви не хочете, щоб одне ядро довгий час перегрівалося від запуску своєї нитки, чи не так? Можливо, заміна на інше ядро дозволить цій ядрі turbo підвищити тактову частоту. (Насправді: вони настільки термічно близькі один до одного, що це вкрай малоймовірно, за винятком багатосистемної системи). Тепер просто помиліться налаштуванням і робіть це занадто часто. Крім часу, витраченого на збереження / відновлення стану потоку ОС, у новому ядрі є холодні кеші L2 / L1, загальний кеш і передбачувачі гілок.
Введення частих непотрібних системних дзвінків може сповільнити вас незалежно від того, якими вони є. Хоча деякі важливі, але прості, такі як gettimeofday
можуть бути реалізовані в просторі користувача з, без переходу в режим ядра. (glibc в Linux робить це за допомогою ядра, оскільки ядро експортує код у vdso
).
Докладніше про накладні витрати на системні виклики (включаючи пропуски кешу / TLB після повернення в користувальницький простір, а не лише контекстний комутатор), на папері FlexSC є великий аналіз поточної ситуації, а також пропозиція щодо створення пакетної системи дзвінки з масово багатопотокових серверних процесів.
while(true){}