Вчера я отслеживал ошибку в своем проекте, которая - через несколько часов - я сузился до фрагмента кода, который более или менее делал что-то вроде это:Общий способ обработки сглаженных значений с плавающей запятой
#include <iostream>
#include <cmath>
#include <cassert>
volatile float r = -0.979541123;
volatile float alpha = 0.375402451;
int main()
{
float sx = r * cosf(alpha); // -0.911326
float sy = r * sinf(alpha); // -0.359146
float ex = r * cosf(alpha); // -0.911326
float ey = r * sinf(alpha); // -0.359146
float mx = ex - sx; // should be 0
float my = ey - sy; // should be 0
float distance = sqrtf(mx * mx + my * my) * 57.2958f; // should be 0, gives 1.34925e-06
// std::cout << "sv: {" << sx << ", " << sy << "}" << std::endl;
// std::cout << "ev: {" << ex << ", " << ey << "}" << std::endl;
// std::cout << "mv: {" << mx << ", " << my << "}" << std::endl;
std::cout << "distance: " << distance << std::endl;
assert(distance == 0.f);
// assert(sx == ex && sy == ey);
// assert(mx == 0.f && my == 0.f);
}
После компиляции и выполнения:
$ g++ -Wall -Wextra -Wshadow -march=native -O2 vfma.cpp && ./a.out
distance: 1.34925e-06
a.out: vfma.cpp:23: int main(): Assertion `distance == 0.f' failed.
Aborted (core dumped)
с моей точки зрения что-то не так, как я просил 2 вычитаний двух пар поразрядными идентичных (я ожидал получить два нули), затем их возведение в квадрат (два нуля снова) и их объединение (ноль).
Оказалось, что основной причиной проблемы является использование операции с объединенным многократным добавлением, которая где-то вдоль линии делает результат неточным (с моей точки зрения). Вообще-то я ничего не имею против этой оптимизации, так как обещает дать результаты, которые больше точнее, но в этом случае 1.34925e-06 действительно далека от ожидаемого мною 0.
Тестовый чехол очень «хрупкий» - если вы включаете больше отпечатков или больше утверждений, он перестает утверждать, потому что компилятор больше не использует fused-multiply-add. Например, если я раскомментировать все строки:
$ g++ -Wall -Wextra -Wshadow -march=native -O2 vfma.cpp && ./a.out
sv: {-0.911326, -0.359146}
ev: {-0.911326, -0.359146}
mv: {0, 0}
distance: 0
Как я считал это ошибка в компиляторе, я сообщил, что, но он был закрыт с объяснением, что это правильное поведение.
https://gcc.gnu.org/bugzilla/show_bug.cgi?id=79436
Так что я задаюсь вопросом - каким образом следует один код таких вычислений, чтобы избежать этой проблемы? Я думал о том, общее решение, но что-то лучше, чем:
mx = ex != sx ? ex - sx : 0.f;
Я хотел бы, чтобы исправить или улучшить свой код - если есть что-нибудь, чтобы исправить/улучшить - вместо установки -ffp-contract=off
для всего моего проекта, fused- multiply-add используется внутри библиотек компилятора в любом случае (я вижу много всего в sinf() и cosf()), поэтому это будет «частичное обходное», а не решение ... Мне также хотелось бы чтобы избежать решений, таких как «не использовать с плавающей точкой» (;
Число плавающей запятой/арифметика потенциально неточно. Это хорошо известная «функция» арифметики с плавающей запятой. – barny
@barny - Я знаю, но для вещей, таких как вычитание двух одинаковых чисел или умножение чего-либо на нулевую арифметику с плавающей запятой, было совершенно точно. «Было» - потому что с fused-multiply-add это уже не так ... Также я думаю, что масштаб ошибки здесь довольно большой. Если бы я получил sth как 1e-64, тогда я бы не стал задавать этот вопрос ... –
Удачи вам в вашем квете. – barny