Насколько выгодно использовать слитное умножение-накопление для двойной точности?

Я пытаюсь понять, выгодно ли использовать std::fma с двойными аргументами, глядя на сгенерированный код сборки, я использую флаг "-O3" и сравниваю сборку для эти две процедуры:

#include <cmath>
#define FP_FAST_FMAF

float test_1(const double &a, const double &b, const double &c ){
    return a*b + c;
}
float test_2(const double &a, const double &b, const double &c ){
    return std::fma(a,b,c);
}

С помощью инструментов Compiler Explorer создается следующая сборка для двух подпрограмм:

test_1(double const&, double const&, double const&):
        movsd     xmm0, QWORD PTR [rdi]                         #5.12
        mulsd     xmm0, QWORD PTR [rsi]                         #5.14
        addsd     xmm0, QWORD PTR [rdx]                         #5.18
        cvtsd2ss  xmm0, xmm0                                    #5.18
        ret                                                     #5.18
test_2(double const&, double const&, double const&):
        push      rsi                                           #7.65
        movsd     xmm0, QWORD PTR [rdi]                         #8.12
        movsd     xmm1, QWORD PTR [rsi]                         #8.12
        movsd     xmm2, QWORD PTR [rdx]                         #8.12
        call      fma                                           #8.12
        cvtsd2ss  xmm0, xmm0                                    #8.12
        pop       rcx                                           #8.12
        ret      

И сборка не меняется при использовании последней доступной версии ни для icc, ни для gcc. Что меня озадачивает в отношении производительности двух подпрограмм, так это то, что в то время как для test_1 есть только одна операция с памятью ( movsd ), для test_2 их три, и, учитывая задержку для операций с памятью, между на один и два порядка больше, чем задержка для операций с плавающей запятой, test_1 должен быть более производительным. Итак, в каких ситуациях целесообразно использовать std::fma? Что ошибочно в моей гипотезе?


person user3116936    schedule 09.06.2020    source источник
comment
На самом деле это не ответ, но если вы удалите ссылки на a, b и c, то сборка для test_2 станет просто вызовом jmp fma, а test_1 станет 3 инструкциями. (Пример в проводнике компилятора)   -  person Human-Compiler    schedule 09.06.2020
comment
опция -O3 ничего не знает о вашем наборе инструкций. Я только что добавил -march=native с обоими компиляторами, и ваши две функции стали эквивалентны (и используйте инструкцию vfmadd213sd). Между прочим, инструкции mulsd и addsd содержат операцию перемещения (т.е. извлечения данных из памяти).   -  person prog-fh    schedule 09.06.2020
comment
все три должны выполнять циклы памяти, как диктует ваш код, одно и то же число. но при использовании функции test_2 может работать медленнее. Если оптимизатор может распознавать умножение-накопление и запрограммирован на его использование, то вызов функции всегда будет медленнее, чем ее генерация компилятором. если он не может оптимизировать это, то он может пойти в любом случае. если вы отрабатываете адреса к вещам, а не сами вещи, то производительность вас не интересует. так как математика делается вторично.   -  person old_timer    schedule 09.06.2020
comment
Ваш заголовок вводит в заблуждение, подразумевая, что вы хотите использовать конкретную инструкцию, но ваша реализация по большей части отбрасывает прирост производительности, который вы могли бы увидеть, сохранив инструкцию. вопрос должен был быть больше похож на то, какие преимущества дает использование функции по сравнению с кодом, генерируемым встроенным.   -  person old_timer    schedule 09.06.2020
comment
Если они обрабатываются встроенно, а не в вызове функции, может произойти много различий — избегание выборки памяти, изменение порядка инструкций, перекрытие и т. д.   -  person Rick James    schedule 09.06.2020
comment
См. также Не работает ли мой fma()?.   -  person chux - Reinstate Monica    schedule 09.06.2020
comment
re: задержка: задержка загрузки L1d составляет от 5 до 6 циклов (для SIMD-загрузок) на текущих процессорах, что примерно соответствует задержке FMA. Ссылка на память является вашей собственной ошибкой для передачи по ссылке (указатели на память), а не по значению (в регистрах XMM). godbolt.org/z/DFgKMz показывает, что GCC будет (по умолчанию) даже сокращаться a*b + c в инструкцию FMA с настройкой по умолчанию -ffp-contract=fast, если она доступна.   -  person Peter Cordes    schedule 09.06.2020


Ответы (1)


Если ваш вопрос связан только с количеством операций с памятью, важно отметить, что mulsd и addsd также являются операциями с памятью в вашем примере. Операции с памятью обозначаются квадратными скобками вокруг имени регистра, а не самой мнемоникой сборки.

Если вам все еще интересно, выгодно ли использовать std::fma, ответ, вероятно, будет «это зависит».

Когда вы анализируете производительность, глядя на сборку, почти необходимо предоставить компилятору хотя бы некоторую информацию о вашей целевой архитектуре. std::fma использует аппаратные инструкции FMA, если они доступны в целевой архитектуре, поэтому вопрос о том, улучшает ли std::fma производительность в целом, на самом деле не является ответом.

Если вы укажете -mfma в Compiler Explorer, у компилятора будет некоторая информация, которую он может использовать для создания более эффективного кода. . Вы также можете указать -march=[your architecture], который автоматически установит для вас -mfma, если он поддерживается.


Кроме того, есть еще целая куча червей по поводу небольших различий в результатах std::fma и (a*b)+c из-за способа округления чисел с плавающей запятой. std::fma выполняет округление только один раз во время двух операций с плавающей запятой, в то время как (a*b)+c может[1] выполнить a*b, сохранить результат в 64-битном формате, добавить c к этому значению, а затем сохранить результат в 64-битном формате.

Если вы хотите свести к минимуму арифметическую ошибку с плавающей запятой в своих вычислениях, std::fma, вероятно, будет лучшим выбором, потому что он гарантирует, что драгоценные биты будут удалены из ваших драгоценных чисел с плавающей запятой только один раз.


[1]Произойдет ли это дополнительное округление, зависит от вашего компилятора, ваших настроек оптимизации и настроек вашей архитектуры: Compiler Explorer примеры для msvc, gcc, icc, clang

person Jeffrey Cash    schedule 09.06.2020
comment
Также стоит отметить, что ссылки на память являются ошибкой OP для передачи по ссылке, а не по значению (уже в регистрах XMM). - person Peter Cordes; 09.06.2020
comment
Но да, если std::fma может быть встроен в одну инструкцию, это обычно лучше для пропускной способности, а иногда и для задержки. (Хотя gcc уже свяжет mul/add в FMA, и у clang есть возможность сделать это агрессивно для операторов, таких как GCC по умолчанию, поэтому вам часто не нужно вручную использовать std::fma.) Но без аппаратной поддержки FMA std::fma массово помедленнее. - person Peter Cordes; 09.06.2020