Ошибка обработки с плавающей точкой в Visual Studio?
Там обновление в конце!
У меня есть небольшая история.
Я хотел вычислить эпсилон компьютера (самый большой эпсилон> 0, удовлетворяющий условию 1.0 + epsilon = 1.0) в программе на C, скомпилированной MS Visual Studio 2008 (работающей в Windows 7 на 64-битном ПК). Поскольку я знаю, что double и float имеют разную точность, я хотел увидеть ответ для обоих. По этой причине я построил следующую программу:
#include <stdio.h>
typedef double float_type;
int main()
{
float_type eps = 1.0;
while ((float_type) 1.0 + eps / (float_type) 2.0 > (float_type) 1.0)
eps = eps / (float_type) 2.0;
printf("%g\n", eps);
return 0;
}
Я был очень удивлен, увидев, что он дал одинаковый ответ для обоих типов double и float: 2.22045e-16. Это было странно, поскольку double занимает вдвое больше памяти, чем float, и должен быть более точным. После этого я заглянул в Википедию и взял оттуда пример кода:
#include <stdio.h>
int main(int argc, char **argv)
{
float machEps = 1.0f;
do {
machEps /= 2.0f;
} while ((float)(1.0 + (machEps/2.0)) != 1.0);
printf( "\nCalculated Machine epsilon: %G\n", machEps );
return 0;
}
Я был еще более удивлен, когда он работал правильно! После некоторых попыток понять фундаментальное различие между этими двумя программами я выяснил следующий факт: моя программа (первая) начинает давать правильный ответ для float (1.19209e-07), если я изменяю условие цикла на
while ((float_type) (1.0 + eps / (float_type) 2.0) > (float_type) 1.0)
Ну, это тайна, которую вы бы сказали. О, настоящая тайна заключается в следующем. Для сравнения:
while ((float) (1.0 + eps / 2.0f) > 1.0f)
который дал правильный ответ (1.19209e-07) и
while ((float) (1.0f + eps / 2.0f) > 1.0f)
который дал ответ, который является неправильным для числа с плавающей запятой и правильным для двойного (2.22045e-16).
На самом деле это совершенно неправильно, результат должен был быть противоположным. Это связано с тем, что по умолчанию константы, такие как 1.0, обрабатываются компилятором как двойные (в соответствии со стандартом), а если они присутствуют в арифметическом выражении, то все остальные операнды переводятся в двойные. И наоборот, когда я пишу 1.0f, все операнды являются плавающими, и никакого продвижения не должно происходить. И все же я получаю совершенно другой результат.
После всех этих тестов я попытался скомпилировать запуск программ на Linux с помощью gcc. Не удивительно, он напечатал именно то, что я ожидал (правильные ответы). Итак, теперь я думаю, что это ошибка Visual Studio. Чтобы заставить вас смеяться (если есть люди, которые до этого момента читали мой пост, что сомнительно ^_^), я приведу еще одно сравнение:
float c = 1.0;
while ((float) (c + eps / 2.0f) > 1.0f)
Это не работает должным образом в VS, но...
const float c = 1.0;
дает правильный ответ 1.19209e-07.
Пожалуйста, кто-нибудь, скажите мне, если я прав, что корень проблемы - глючный компилятор VS 2008 (можете ли вы подтвердить ошибку на своих машинах?). Я был бы также признателен, если бы вы протестировали корпус в более новой версии: MS VS 2010. Спасибо.
ОБНОВИТЬ. С MS Visual Studio 2013 первая упомянутая мной программа работает без неожиданных результатов - она дает соответствующие ответы для float и double. Я проверил это на всех моделях с плавающей запятой (точных, строгих и быстрых), и ничего не изменилось. Так что действительно кажется, что VS 2008 глючил в этом случае.
1 ответ
По умолчанию параметр с плавающей точкой в Visual Studio установлен на "точный". Это означает, что он попытается сделать результат как можно более точным. Одним из побочных эффектов этого является то, что промежуточные продукты повышаются до двойной точности.
Несмотря на то, что я не проверял каждый фрагмент кода, который вы разместили, я подозреваю, что проблема здесь:
(float) (c + eps / 2.0f)
c + eps / 2.0f
делается с использованием двойной точности. Каждый из 3 операндов повышается до двойной точности, и все выражение оценивается как таковое. Он округляется до числа с плавающей точкой, когда вы его разыгрываете.
Если вы установите режим с плавающей точкой "строгий", он должен работать так, как вы ожидаете.