Когда я впервые получил процессор Haswell, я попробовал реализовать FMA для определения набора Мандельброта. Основной алгоритм таков:
intn = 0;
for(int32_t i=0; i<maxiter; i++) {
floatn x2 = square(x), y2 = square(y); //square(x) = x*x
floatn r2 = x2 + y2;
booln mask = r2<cut; //booln is in the float domain non integer domain
if(!horizontal_or(mask)) break; //_mm256_testz_pd(mask)
n -= mask
floatn t = x*y; mul2(t); //mul2(t): t*=2
x = x2 - y2 + cx;
y = t + cy;
}
Это определяет, находятся ли n
пиксели в наборе Мандельброта. Таким образом, для двойной с плавающей запятой она работает на 4 пикселя (floatn = __m256d
, intn = __m256i
). Это требует 4 SIMD-операций с плавающей запятой и четырех добавок с плавающей запятой SIMD.
Затем я изменил это, чтобы работать с FMA, как это
intn n = 0;
for(int32_t i=0; i<maxiter; i++) {
floatn r2 = mul_add(x,x,y*y);
booln mask = r2<cut;
if(!horizontal_or(mask)) break;
add_mask(n,mask);
floatn t = x*y;
x = mul_sub(x,x, mul_sub(y,y,cx));
y = mul_add(2.0f,t,cy);
}
где mul_add вызывает _mm256_fmad_pd
и mul_sub вызывает _mm256_fmsub_pd
. Этот метод использует 4 операции SIM-карты FMA и два SIMD-умножения, которые являются двумя менее арифметическими операциями, а затем без FMA. Кроме того, FMA и умножение могут использовать два порта и добавить только один.
Чтобы мои тесты были менее предвзятыми, я увеличил масштаб до области, которая полностью находится в наборе Мандельброта, поэтому все значения maxiter
. В этом случае метод, использующий FMA, примерно на 27% быстрее.. Конечно, улучшение, но переход от SSE к AVX удвоил мою производительность, поэтому я надеялся, что, возможно, еще один фактор из двух с FMA.
Но затем я нашел этот ответ в отношении FMA, где он говорит
Важным аспектом инструкции с объединенным умножением-добавлением является (практически) бесконечная точность промежуточного результата. Это помогает в производительности, но не столько потому, что две операции кодируются в одной команде - это помогает в производительности, потому что практически бесконечная точность промежуточного результата иногда важна и очень дорога для восстановления с обычным умножением и добавлением, когда этот уровень точность - это то, что программист после.
а затем приводит пример double * double to double-double умножение
high = a * b; /* double-precision approximation of the real product */
low = fma(a, b, -high); /* remainder of the real product */
Из этого я пришел к выводу, что я внедряю FMA не оптимально, поэтому решил реализовать SIMD double-double. Я реализовал double-double на основе Числа с плавающей запятой с расширенной точностью для вычисления графических процессоров. Бумага предназначена для двойного плавания, поэтому я изменил ее для двойного двойного. Кроме того, вместо того, чтобы упаковывать одно двойное значение в регистры SIMD, я упаковываю 4 двойных значения в один высокий регистр AVX и один низкий регистр AVX.
Для набора Мандельброта то, что мне действительно нужно, это двойное двойное умножение и добавление. В этой статье это функции df64_add
и df64_mult
.
На рисунке ниже показана сборка для моей функции df64_mult
для программного обеспечения FMA (слева) и аппаратного FMA (справа). Это ясно показывает, что аппаратное FMA является большим улучшением для двойного двойного умножения.
Итак, как аппаратное FMA выполняет в двойном двойном вычислении Мандельброта? Ответ заключается в том, что только на 15% быстрее, чем с программным обеспечением FMA. Это намного меньше, чем я надеялся. Для вычисления двойного двойника Мандельброта требуется 4 двойных двойных дополнения и четыре двойных двойных умножения (x*x
, y*y
, x*y
и 2*(x*y)
). Тем не менее, 2*(x*y)
умножение тривиально для double-double, поэтому это умножение можно игнорировать в стоимости. Поэтому причина, по которой я думаю, что улучшение с использованием аппаратного FMA настолько мало, заключается в том, что в расчете преобладает медленное двойное двойное добавление (см. Сборку ниже).
Раньше было, что умножение было медленнее, чем добавление (и программисты использовали несколько трюков, чтобы избежать умножения), но с Haswell кажется, что это наоборот. Не только из-за FMA, но и потому, что умножение может использовать два порта, но только одно.
Итак, мои вопросы (наконец):
- Как оптимизировать, когда добавление медленное по сравнению с умножением?
- Есть ли алгебраический способ изменить мой алгоритм, чтобы использовать больше умножений
и меньше дополнений? Я знаю, что есть способ сделать обратное, например.
(x+y)*(x+y) - (x*x+y*y) = 2*x*y
, которые используют еще два дополнения для одного меньшего умножения. - Есть ли способ просто функции df64_add (например, с использованием FMA)?
В случае, если кто-то задается вопросом, что двойной двойной метод примерно в десять раз медленнее, чем двойной. Это не так плохо, я думаю, что, если бы существовал аппаратный четырехступенчатый тип, он, вероятно, был бы как минимум в два раза медленнее, чем двойной, поэтому мой программный метод примерно в пять раз медленнее, чем я ожидал бы для аппаратного обеспечения, если бы он существовал.
df64_add
сборка
vmovapd 8(%rsp), %ymm0
movq %rdi, %rax
vmovapd 72(%rsp), %ymm1
vmovapd 40(%rsp), %ymm3
vaddpd %ymm1, %ymm0, %ymm4
vmovapd 104(%rsp), %ymm5
vsubpd %ymm0, %ymm4, %ymm2
vsubpd %ymm2, %ymm1, %ymm1
vsubpd %ymm2, %ymm4, %ymm2
vsubpd %ymm2, %ymm0, %ymm0
vaddpd %ymm1, %ymm0, %ymm2
vaddpd %ymm5, %ymm3, %ymm1
vsubpd %ymm3, %ymm1, %ymm6
vsubpd %ymm6, %ymm5, %ymm5
vsubpd %ymm6, %ymm1, %ymm6
vaddpd %ymm1, %ymm2, %ymm1
vsubpd %ymm6, %ymm3, %ymm3
vaddpd %ymm1, %ymm4, %ymm2
vaddpd %ymm5, %ymm3, %ymm3
vsubpd %ymm4, %ymm2, %ymm4
vsubpd %ymm4, %ymm1, %ymm1
vaddpd %ymm3, %ymm1, %ymm0
vaddpd %ymm0, %ymm2, %ymm1
vsubpd %ymm2, %ymm1, %ymm2
vmovapd %ymm1, (%rdi)
vsubpd %ymm2, %ymm0, %ymm0
vmovapd %ymm0, 32(%rdi)
vzeroupper
ret