Когда я впервые получил процессор 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
