Будьте дуже обережні з розподілом і уникайте його, коли це можливо. Наприклад, підняти float inverse = 1.0f / divisor;
з петлі і помножити на inverse
внутрішню петлю. (Якщо помилка округлення в inverse
допустима)
Зазвичай 1.0/x
не може бути точно представленим як float
або double
. Це точно буде , коли x
це сила 2. Це дозволяє компілятори Оптимізувати x / 2.0f
для x * 0.5f
без будь - яких змін в результаті.
Щоб дозволити компілятору виконати цю оптимізацію за вас, навіть коли результат не буде точним (або з дільником змінної виконання), вам потрібні такі опції gcc -O3 -ffast-math
. В Зокрема, -freciprocal-math
(включається -funsafe-math-optimizations
включається -ffast-math
) дозволяє компілятору замінити x / y
з , x * (1/y)
коли це корисно. Інші компілятори мають подібні параметри, і ICC може ввімкнути деяку "небезпечну" оптимізацію за замовчуванням (я думаю, що це робить, але я забуваю).
-ffast-math
часто важливо дозволити автоматичну векторизацію циклів FP, особливо скорочень (наприклад, підсумовування масиву в один скалярний підсумок), оскільки математика FP не асоціативна. Чому GCC не оптимізує a * a * a * a * a * a до (a * a * a) * (a * a * a)?
Також зверніть увагу , що C ++ компілятор можна скласти +
і *
в FMA в деяких випадках (при компіляції для мети , яка підтримує його, як -march=haswell
), але вони не можуть зробити це з /
.
Час затримки підрозділу гірший, ніж множення або додавання (або FMA ) на коефіцієнт від 2 до 4 на сучасних процесорах x86, і гірша пропускна здатність у 6 до 40 1 (для вузького циклу, що виконує лише ділення, а не лише множення).
Блок поділу / sqrt не повністю конвеєрний, з причин, пояснених у відповіді @ NathanWhitehead . Найгірші коефіцієнти відносяться до векторів 256b, оскільки (на відміну від інших одиниць виконання) одиниця поділу, як правило, не на всю ширину, тому широкі вектори доводиться робити у дві половини. Не повністю конвеєрний блок виконання настільки незвичний, що процесори Intel мають arith.divider_active
апаратний лічильник продуктивності, який допоможе вам знайти код, який вузькими місцями на пропускній здатності дільника, замість звичайних вузьких місць у інтерфейсі або порту виконання. (Або частіше вузькі місця в пам'яті або довгі ланцюжки затримок, що обмежують паралелізм на рівні інструкцій, спричиняючи пропускну здатність інструкцій менше ~ 4 за такт).
Однак розділення FP і sqrt на процесорах Intel і AMD (крім KNL) реалізовані як єдине загальне, тому це не обов'язково має великий вплив на пропускну здатність на оточуючий код . Найкращий випадок для поділу - це коли невпорядковане виконання може приховати затримку, і коли є багато множень та додавання (або інша робота), які можуть відбуватися паралельно з поділом.
(Цілочисельний розподіл мікрокодується як багаторазове передавання даних на Intel, тому він завжди має більший вплив на навколишній код, який цілочисельне множиться. Попит на високопродуктивний цілочисельний поділ менше, тому апаратна підтримка не така вже й фантазійна. Пов’язані: мікрокодовані інструкції, такі як idiv
can спричиняють вирівнювання інтерфейсних вузьких місць .)
Так, наприклад, це буде дуже погано:
for ()
a[i] = b[i] / scale;
float inv = 1.0 / scale;
for ()
a[i] = b[i] * inv;
Все, що ви робите в циклі, - це завантаження / розділення / зберігання, і вони незалежні, тому має значення пропускна здатність, а не затримка.
Скорочення, як accumulator /= b[i]
би, було вузьким місцем щодо поділу або множення затримки, а не пропускної здатності. Але за допомогою кількох акумуляторів, які ви ділите або множите в кінці, ви можете приховати затримку і все одно наситити пропускну здатність. Зверніть увагу, що sum += a[i] / b[i]
вузькі місця щодо add
затримки або div
пропускної здатності, але не div
затримки, оскільки розподіл не знаходиться на критичному шляху (ланцюжок залежностей, що несеться з циклу).
Але приблизно вlog(x)
цьому ( апроксимуючи функцію на зразок відношення двох поліномів ), поділ може бути досить дешевим :
for () {
float p = polynomial(b[i], 1.23, -4.56, ...);
float q = polynomial(b[i], 3.21, -6.54, ...);
a[i] = p/q;
}
Для log()
більшого діапазону мантиси відношення двох поліномів порядку N має набагато меншу похибку, ніж окремий поліном з коефіцієнтами 2N, і паралельне обчислення 2 дає певний паралелізм на рівні інструкцій у межах одного тіла циклу замість одного масивно довгого Dep ланцюга, що робить речі НАБАГАТО простішими для виконання поза порядку.
У цьому випадку ми не створюємо вузьких місць щодо затримки поділу, оскільки невпорядковане виконання може зберігати кілька ітерацій циклу над масивами в польоті.
Ми не створюємо вузьких місць щодо пропускної здатності поділу , якщо наші поліноми достатньо великі, щоб у нас був лише один розділ на кожні 10 інструкцій FMA або близько того. (А у випадку реального log()
використання існує купа робіт з вилучення експоненти / мантиси та комбінування речей знову, тому між розділеннями ще більше роботи.)
Коли все-таки потрібно розділити, зазвичай краще просто розділити замість rcpps
x86 має наближену взаємну інструкцію ( rcpps
), яка дає лише 12 біт точності. (AVX512F має 14 бітів, а AVX512ER - 28 бітів.)
Ви можете використовувати це, x / y = x * approx_recip(y)
не використовуючи фактичну інструкцію розділення. ( rcpps
itsef досить швидкий; зазвичай трохи повільніший, ніж множення. Він використовує пошук таблиці з внутрішньої таблиці до центрального процесора. Обладнання розділювача може використовувати ту саму таблицю як вихідну точку.)
Для більшості цілей x * rcpps(y)
занадто неточна, і потрібна ітерація Ньютона-Рафсона для подвоєння точності. Але це коштує вам 2 множень і 2 FMA , і затримка приблизно така ж велика, як фактична інструкція поділу. Якщо все, що ви робите, це поділ, то це може бути перемогою в пропускній здатності. (Але спочатку слід уникати такого циклу, якщо можете, можливо, роблячи поділ як частину іншого циклу, який виконує іншу роботу.)
Але якщо ви використовуєте поділ як частину більш складної функції, rcpps
сам + додатковий mul + FMA, як правило, пришвидшує просто поділ за допомогою divps
інструкції, за винятком процесорів з дуже низькою divps
пропускною здатністю.
(Наприклад, Knight's Landing, див. Нижче. KNL підтримує AVX512ER , тому для float
векторів VRCP28PS
результат вже достатньо точний, щоб просто множитися без ітерації Ньютона-Рафсона. float
Розмір мантиси становить лише 24 біти.)
Конкретні числа з таблиць Агнера Фога:
На відміну від будь-якої іншої операції ALU, затримка / пропускна здатність поділу залежить від даних деяких процесорів. Знову ж таки, це тому, що він настільки повільний і не повністю завершений. Диспетчеризація, яка не працює в порядку, простіша з фіксованими затримками, оскільки вона дозволяє уникнути конфліктів зворотної записи (коли один і той же порт виконання намагається отримати 2 результати в тому ж циклі, наприклад, запустити 3-циклову інструкцію, а потім дві 1-циклові операції) .
Як правило, найшвидші випадки, коли дільник є "круглим" числом, подібним 2.0
чи 0.5
(тобто представлення base2 float
має безліч кінцевих нулів у мантисі).
float
затримка (цикли) / пропускна здатність (цикли за інструкцією, виконуючи лише це ззаду до спини з незалежними входами):
scalar & 128b vector 256b AVX vector
divss | mulss
divps xmm | mulps vdivps ymm | vmulps ymm
Nehalem 7-14 / 7-14 | 5 / 1 (No AVX)
Sandybridge 10-14 / 10-14 | 5 / 1 21-29 / 20-28 (3 uops) | 5 / 1
Haswell 10-13 / 7 | 5 / 0.5 18-21 / 14 (3 uops) | 5 / 0.5
Skylake 11 / 3 | 4 / 0.5 11 / 5 (1 uop) | 4 / 0.5
Piledriver 9-24 / 5-10 | 5-6 / 0.5 9-24 / 9-20 (2 uops) | 5-6 / 1 (2 uops)
Ryzen 10 / 3 | 3 / 0.5 10 / 6 (2 uops) | 3 / 1 (2 uops)
Low-power CPUs:
Jaguar(scalar) 14 / 14 | 2 / 1
Jaguar 19 / 19 | 2 / 1 38 / 38 (2 uops) | 2 / 2 (2 uops)
Silvermont(scalar) 19 / 17 | 4 / 1
Silvermont 39 / 39 (6 uops) | 5 / 2 (No AVX)
KNL(scalar) 27 / 17 (3 uops) | 6 / 0.5
KNL 32 / 20 (18uops) | 6 / 0.5 32 / 32 (18 uops) | 6 / 0.5 (AVX and AVX512)
double
затримка (цикли) / пропускна здатність (цикли за інструкцією):
scalar & 128b vector 256b AVX vector
divsd | mulsd
divpd xmm | mulpd vdivpd ymm | vmulpd ymm
Nehalem 7-22 / 7-22 | 5 / 1 (No AVX)
Sandybridge 10-22 / 10-22 | 5 / 1 21-45 / 20-44 (3 uops) | 5 / 1
Haswell 10-20 / 8-14 | 5 / 0.5 19-35 / 16-28 (3 uops) | 5 / 0.5
Skylake 13-14 / 4 | 4 / 0.5 13-14 / 8 (1 uop) | 4 / 0.5
Piledriver 9-27 / 5-10 | 5-6 / 1 9-27 / 9-18 (2 uops) | 5-6 / 1 (2 uops)
Ryzen 8-13 / 4-5 | 4 / 0.5 8-13 / 8-9 (2 uops) | 4 / 1 (2 uops)
Low power CPUs:
Jaguar 19 / 19 | 4 / 2 38 / 38 (2 uops) | 4 / 2 (2 uops)
Silvermont(scalar) 34 / 32 | 5 / 2
Silvermont 69 / 69 (6 uops) | 5 / 2 (No AVX)
KNL(scalar) 42 / 42 (3 uops) | 6 / 0.5 (Yes, Agner really lists scalar as slower than packed, but fewer uops)
KNL 32 / 20 (18uops) | 6 / 0.5 32 / 32 (18 uops) | 6 / 0.5 (AVX and AVX512)
Айвібрідж і Бродвелл теж різні, але я хотів, щоб стіл був невеликим. (Core2 (до Nehalem) має кращу продуктивність дільника, але його максимальна тактова частота була нижчою.)
Atom, Silvermont і навіть Knight's Landing (Xeon Phi на базі Silvermont) мають надзвичайно низьку продуктивність поділу , і навіть 128b вектор повільніший за скалярний. Низькопотужний процесор AMD Jaguar (використовується в деяких консолях) подібний. Високопродуктивний дільник займає велику площу штампа. Xeon Phi має низьку потужність на ядро , а упаковка великої кількості ядер на матриці забезпечує жорсткіші обмеження площі матриці, ніж Skylake-AVX512. Здається, AVX512ER rcp28ps
/ pd
- це те, що ви "повинні" використовувати на KNL.
(Див. Цей результат InstLatx64 для Skylake-AVX512 aka Skylake-X. Номери для vdivps zmm
: 18c / 10c, тобто половина пропускної здатності ymm
.)
Довгі ланцюжки затримок стають проблемою, коли вони несуть петлю або коли вони настільки довгі, що зупиняють невпорядковане виконання і не знаходять паралельності з іншою самостійною роботою.
Примітка 1: як я склав ці співвідношення продуктивності div та mul:
Коефіцієнт поділу FP та множинної продуктивності навіть гірший, ніж у центральних процесорів з низьким енергоспоживанням, таких як Silvermont та Jaguar, і навіть у Xeon Phi (KNL, де слід використовувати AVX512ER).
Фактичні коефіцієнти ділення / множення пропускної здатності для скалярів (без векторів)double
: 8 на Ryzen і Skylake з підсиленими дільниками, але 16-28 на Haswell (залежно від даних і, швидше за все, до кінця 28 циклу, якщо ваші дільники не круглі цифри). Ці сучасні центральні процесори мають дуже потужні роздільники, але їх множинна пропускна здатність 2 на годину це здуває. (Навіть більше, коли ваш код може автоматично векторизуватись за допомогою векторів 256b AVX). Також зауважте, що при правильних параметрах компілятора ця множинна пропускна здатність також застосовується до FMA.
Номери з http://agner.org/optimize/ таблиць інструкцій для Intel Haswell / Skylake та AMD Ryzen, для скаляра SSE (не враховуючи x87 fmul
/ fdiv
) та для векторів 256b AVX SIMD з float
або double
. Див. Такожx86 тег wiki.