Будьте очень осторожны с разделением и по возможности избегайте его. Например, вытащите float inverse = 1.0f / divisor;
из цикла и умножьте на inverse
внутри цикла. (Если ошибка округления в inverse
допустима)
Обычно 1.0/x
не может быть точно представлен как float
или double
. Это будет точно, когда x
будет степенью 2. Это позволяет компиляторам оптимизировать x / 2.0f
до x * 0.5f
без каких-либо изменений в результате.
Чтобы компилятор мог выполнить эту оптимизацию за вас, даже если результат не будет точным (или с делителем переменной времени выполнения), вам потребуются такие параметры, как _ 10_. В частности, -freciprocal-math
(включено -funsafe-math-optimizations
включено -ffast-math
) позволяет компилятору заменять x / y
на x * (1/y)
, когда это полезно. Другие компиляторы имеют аналогичные параметры, и ICC может включать некоторую «небезопасную» оптимизацию по умолчанию (я думаю, что это так, но я забываю).
-ffast-math
часто важен для обеспечения автоматической векторизации циклов FP, особенно сокращений (например, суммирование массива в одну скалярную сумму), потому что математика FP не ассоциативна. Почему GCC не оптимизирует a * a * a * a * a * a до (а * а * а) * (а * а * а)?
Также обратите внимание, что компиляторы C ++ могут сворачивать +
и *
в FMA в некоторых случаях (при компиляции для поддерживающей его цели, например -march=haswell
), но они не могут этого сделать с /
.
У деления задержка хуже, чем у умножения или сложения (или FMA) в 2 раза. 4 на современных процессорах x86 и худшая пропускная способность в 6-40 раз 1 (для плотного цикла, выполняющего только деление вместо только < / em> умножение).
Блок div / sqrt не является полностью конвейерным по причинам, описанным в Ответ @ NathanWhitehead. Наихудшие соотношения для векторов размером 256 байт, потому что (в отличие от других исполнительных единиц) блок деления обычно не является полноширинным, поэтому широкие векторы должны быть разделены на две половины. Не полностью конвейерная исполнительная единица настолько необычна, что в процессорах Intel есть аппаратный счетчик производительности arith.divider_active
, который поможет вам найти код, который ограничивает пропускную способность делителя, а не узкие места обычного интерфейса или порта выполнения. (Или, чаще, узкие места в памяти или длинные цепочки задержек, ограничивающие параллелизм на уровне команд, из-за чего пропускная способность составляет менее ~ 4 за такт).
Однако разделение FP и sqrt на процессорах Intel и AMD (кроме KNL) реализовано как единый uop, поэтому это не обязательно оказывает большое влияние на пропускную способность окружающего кода. Наилучший случай для деления - это когда выполнение вне очереди может скрыть задержку, и когда есть много умножений и сложений (или другой работы), которые могут происходить параллельно с делением.
(Целочисленное деление микрокодировано в Intel как несколько мопов, поэтому оно всегда оказывает большее влияние на окружающий код, чем целочисленное умножение. Высокопроизводительное целочисленное деление менее востребовано, поэтому аппаратная поддержка не такая изысканная. Связано: idiv, могут вызвать чувствительный к выравниванию интерфейс. узкие места.)
Так, например, это будет очень плохо:
for ()
a[i] = b[i] / scale; // division throughput bottleneck
// Instead, use this:
float inv = 1.0 / scale;
for ()
a[i] = b[i] * inv; // multiply (or store) throughput bottleneck
Все, что вы делаете в цикле, - это загрузка / разделение / сохранение, и они независимы, поэтому важна пропускная способность, а не задержка.
Такое сокращение, как accumulator /= b[i]
, будет ограничивать задержку деления или умножения, а не пропускную способность. Но с несколькими накопителями, которые вы делите или умножаете в конце, вы можете скрыть задержку и при этом увеличить пропускную способность. Обратите внимание, что sum += a[i] / b[i]
узкие места по add
задержке или div
пропускной способности, но не div
задержке, потому что разделение не находится на критическом пути (цепочка зависимостей, переносимая петлей).
Но в чем-то вроде этого (аппроксимация функции типа log(x)
соотношением двух многочленов) деление может быть довольно дешево:
for () {
// (not shown: extracting the exponent / mantissa)
float p = polynomial(b[i], 1.23, -4.56, ...); // FMA chain for a polynomial
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
+ дополнительный множитель + 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. Numbers-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 на такт просто сногсшибательна. (Тем более, когда ваш код может автоматически векторизоваться с 256-битными векторами AVX). Также обратите внимание, что при правильных параметрах компилятора это умножение пропускной способности также применимо к FMA.
Числа из таблиц инструкций http://agner.org/optimize/ для Intel Haswell / Skylake и AMD Ryzen для Скаляр SSE (не включая x87 fmul
/ fdiv
) и для 256b AVX SIMD-векторов float
или double
. См. Также вики-страницу с тегами x86.
person
Peter Cordes
schedule
26.08.2017
/ 2.0
в* 0.5
. - person Gabe   schedule 08.11.2010200f / 2
в100f
. - person mikerobi   schedule 08.11.2010