Я написал небольшую программу для вычисления евклидовой нормы трехкоординатного вектора. Вот:
#include <array>
#include <cmath>
#include <iostream>
template<typename T, std::size_t N>
auto norm(const std::array<T, N>& arr)
-> T
{
T res{};
for (auto value: arr)
{
res += value * value;
}
return std::sqrt(res);
}
int main()
{
std::array<double, 3u> arr = { 4.0, -2.0, 6.0 };
std::cout << norm(arr) - norm(arr) << '\n';
}
На моем компьютере он печатает -1.12323e-016
.
Я знаю, что с типами с плавающей запятой следует обращаться осторожно. Однако я думал, что операции с плавающей запятой хоть как-то детерминированы. В В этой статье о детерминизме с плавающей запятой говорится, что:
Некоторые из гарантированных результатов — это результаты сложения, вычитания, умножения, деления и извлечения квадратного корня. Результаты этих операций гарантированно будут точно округлены правильно (подробнее об этом позже), поэтому, если вы предоставите одни и те же входные значения с теми же глобальными настройками и той же точностью назначения, вам гарантирован тот же результат.
Как видите, единственные операции, которые эта программа выполняет со значениями с плавающей запятой, — это сложение, вычитание, умножение и квадратный корень. Если я доверяю статье, которую я цитировал выше, учитывая, что она выполняется в одном потоке и что я не меняю режимы округления или что-либо еще, связанное с плавающей запятой, я подумал, что norm(arr) - norm(arr)
будет 0
, поскольку я выполняю точно такие же операции над одинаковые значения дважды.
Являются ли мои предположения неверными, или это случай, когда компилятор не строго соответствует математике с плавающей запятой IEEE? В настоящее время я использую MinGW-W64 GCC 4.9.1 32 бита (я пробовал все уровни оптимизации от -O0
до -O3
). Та же программа с MinGW-W64 GCC 4.8.x отображала 0
, чего я и ожидал.
EDIT: я дизассемблировал код. Я не буду публиковать всю сгенерированную сборку, потому что она слишком большая. Тем не менее, я считаю, что соответствующая часть здесь:
call ___main
fldl LC0
fstpl -32(%ebp)
fldl LC1
fstpl -24(%ebp)
fldl LC2
fstpl -16(%ebp)
leal -32(%ebp), %eax
movl %eax, (%esp)
call __Z4normIdLj3EET_RKSt5arrayIS0_XT0_EE
fstpl -48(%ebp)
leal -32(%ebp), %eax
movl %eax, (%esp)
call __Z4normIdLj3EET_RKSt5arrayIS0_XT0_EE
fsubrl -48(%ebp)
fstpl (%esp)
movl $__ZSt4cout, %ecx
call __ZNSolsEd
subl $8, %esp
movl $10, 4(%esp)
movl %eax, (%esp)
call __ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_c
movl $0, %eax
movl -4(%ebp), %ecx
.cfi_def_cfa 1, 0
leave
Как видите, __Z4normIdLj3EET_RKSt5arrayIS0_XT0_EE
вызывается дважды, поэтому он не встроен. Я не понимаю всего этого, хотя и не могу сказать, в чем проблема.
T res{};
инициализируетres
в0
? - person François Moisan   schedule 17.09.2014The same program with MinGW-W64 GCC 4.8.x displayed 0
- по крайней мере на 4.8.0 у меня тоже получается-1.12323e-016
- person Andreas Fester   schedule 17.09.2014-Of
или-Og
, а все уровни оптимизации от-O0
и-O3
). И я убедился, что не использую-ffast-math
или эквиваленты. - person Morwenn   schedule 17.09.2014long double
дает ожидаемый результат (даже на 32-битной версии). - person Niall   schedule 17.09.2014SSE
, а 32-битный код — нет. - person Andreas Fester   schedule 17.09.2014