Ошибка обработки с плавающей точкой в ​​Visual Studio? - PullRequest
5 голосов
/ 13 сентября 2011

В конце обновления!

У меня есть небольшая история.

Я хотел вычислить машинный эпсилон (самый большой эпсилон> 0, удовлетворяющий условию 1.0 + эпсилон = 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. Спасибо.

UPDATE. С MS Visual Studio 2013 первая упомянутая мной программа работает без неожиданных результатов - она ​​дает соответствующие ответы для float и double. Я проверил это на всех моделях с плавающей запятой (точных, строгих и быстрых), и ничего не изменилось. Так что действительно кажется, что в этом случае VS 2008 глючил.

1 Ответ

1 голос
/ 13 сентября 2011

По умолчанию для Visual Studio параметр с плавающей запятой установлен на «точный». Это означает, что он попытается сделать результат как можно более точным. Одним из побочных эффектов этого является то, что промежуточные продукты повышаются до двойной точности.

Несмотря на то, что я не посмотрел каждый код, который вы разместили, я подозреваю, что проблема здесь:

(float) (c + eps / 2.0f)

c + eps / 2.0f выполняется с двойной точностью. Каждый из 3 операндов повышается до двойной точности, и все выражение оценивается как таковое. Он округляется до числа с плавающей точкой только когда вы его разыгрываете.

Если вы установите режим с плавающей запятой в «строгий», он должен работать так, как вы ожидаете.

...