cosf (M_PI_2) не возвращает ноль - PullRequest
3 голосов
/ 22 октября 2009

Это началось внезапно сегодня утром.

Оригинальные строки были такими

float angle = (x+90)*(M_PI/180.0);
float xx = cosf(angle);
float yy = sinf(angle);

После установки точки останова и наведения курсора. Я получаю правильный ответ для yy как 1., но xx НЕ равен нулю.

Я пытался с cosf(M_PI_2); все еще не повезло ... до вчерашнего дня все работало нормально.

Я использую последнюю версию Xcode на сегодняшний день

Ответы [ 6 ]

10 голосов
/ 22 октября 2009

Первое, что следует заметить, это то, что вы используете float s. Они по своей сути неточны, и для большинства расчетов дают лишь приблизительное приближение к математически правильному ответу. Предполагая, что x в вашем коде имеет значение 0, angle будет иметь близкое приближение к π / 2. Следовательно, xx будет иметь приближение к cos (π / 2). Однако вряд ли это будет точно ноль из-за проблем аппроксимации и округления.

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

Если это не решит вашу конкретную проблему, сообщите нам более подробную информацию, и мы еще подумаем.

9 голосов
/ 23 октября 2009

Вопреки тому, что говорили другие, это не проблема сопроцессора x87. XCode по умолчанию использует SSE для вычислений с плавающей запятой в Intel (за исключением long double арифметики).

«Проблема» заключается в следующем: когда вы пишете cosf(M_PI_2), вы на самом деле говорите компилятору XCode (gcc или llvm-gcc или clang) сделать следующее:

  1. Посмотрите на расширение M_PI_2 в <math.h>. Согласно стандарту POSIX, это литерал двойной точности, который преобразуется в правильно округленное значение π / 2.
  2. Округление преобразованного значения двойной точности до одинарной точности.
  3. Вызвать функцию математической библиотеки cosf для значения одинарной точности.

Обратите внимание, что на протяжении всего этого процесса вы не работаете с фактическим значением π / 2. Вместо этого вы работаете с этим значением, округленным до представимого числа с плавающей точкой. Хотя cos (π / 2) равно точно нулю, вы не говорите компилятору делать это вычисление. Вместо этого вы указываете компилятору сделать cos (π / 2 + tiny), где tiny - это разница между округленным значением (float)M_PI_2 и (непредставленным) точным значением π / 2. Если cos вычислено безо всякой ошибки, результат cos (π / 2 + tiny) будет примерно -tiny. Если он вернул ноль, , что будет ошибкой.

edit: пошаговое расширение вычислений на Intel Mac с текущим компилятором XCode:

M_PI_2 определяется как

1.57079632679489661923132169163975144

но на самом деле это не представимое число двойной точности. Когда компилятор преобразует его в значение двойной точности, оно становится равным

1.5707963267948965579989817342720925807952880859375

Это самое близкое число двойной точности к π / 2, но оно отличается от фактического математического значения π / 2 примерно на 6,12 * 10 ^ (- 17).

Step (2) округляет это число до одинарной точности, что меняет значение точно на

1.57079637050628662109375

Что примерно равно π / 2 + 4,37 * 10 ^ (- 8). Когда мы вычислим cosf этого числа, то получим:

-0.00000004371138828673792886547744274139404296875

, что очень почти точное значение косинуса, оцененного в этой точке:

-0.00000004371139000186241438857289400265215231661...

На самом деле это правильно округленный результат ; нет никакого значения, которое вычисление могло бы возвратить, которое было бы более точным. Единственная ошибка здесь в том, что вычисления, которые вы просили выполнить компилятор, отличаются от вычислений, которые вы думали , которые вы просили это сделать.

6 голосов
/ 22 октября 2009

Я подозреваю, что ответ настолько близок к 0, что об этом не стоит беспокоиться.

Если я пропущу ту же самую вещь, я получу ответ «-4.3711388e-008», который также можно записать как «-0.000000043711388». Это чертовски близко к 0. Определенно достаточно близко, чтобы не беспокоиться о том, что он находится на восьмом знаке после запятой.

Редактировать: В дополнение к тому, что говорит LiraLuna, я написал следующий фрагмент ассемблера x87 для visual studio

    float fRes;
_asm
{
    fld1
    fld1
    fadd st, st(1)
    fldpi
    fdiv st, st(1)
    fcos
    fstp [fRes]
}
char str[16];
sprintf( str, "%f", fRes );

В основном это использует инструкцию f87 для создания косинуса пи / 2. значение в str равно «0.000000»

Это, однако, на самом деле не то, что вернул fcos. Это на самом деле вернул 6.1230318e-017. Это означает, что ошибка возникает с 17-го знака после запятой и, честно говоря, это гораздо менее значимо, чем стандартная отладка cosf выше.

Поскольку в SSE3 нет специальной косинусной инструкции, я подозреваю (хотя я не могу подтвердить, не увидев сгенерированный ассемблер), что он либо использует свое собственное расширение ряда Тейлора, либо использует инструкцию fcos в любом случае. В любом случае, вы все равно вряд ли получите лучшую точность, чем ошибка, возникающая с 17-го знака после запятой, на мой взгляд.

3 голосов
/ 22 октября 2009

Единственное, о чем я могу думать, это вредоносная замена макроса, т.е. M_PI_2 больше не является 1.57079632679489661923.

Попробуйте позвонить cosf( 1.57079632679489661923 ), чтобы проверить это.

1 голос
/ 22 октября 2009

Реальная вещь, с которой вам следует быть осторожным, это знак косинуса. Убедитесь, что это так же, как вы ожидали. Например. если вы работаете с углами от 0 до пи / 2. сделайте уверенным , что то, что вы используете как PI_2, меньше действительного значения pi / 2 !

И разница между 0.000001 и 0.0 меньше, чем вы думаете.

0 голосов
/ 22 октября 2009

Причина

То, что вы испытываете, - это печально известная x87 математический сопроцессор усеченная с плавающей точкой «ошибка» - или, скорее, - функция. IEEE float имеют удивительный диапазон чисел, но по цене. Они жертвуют прецессией ради высокой дальности.

Они не являются неточными, как вы думаете, хотя - это полумиф, ​​созданный чипом Intel x87, который внутренне использует 80-битное внутреннее представление для чисел с плавающей запятой - они имеют гораздо более высокую прецессию, хотя и немного медленнее.

Когда вы выполняете сравнение с плавающей точкой, x87 кэширует число с плавающей точкой как 80-битное с плавающей точкой, затем, когда его стек заполнен, он сохраняет 32-битное представление в ОЗУ, значительно снижая точность.

Решение

x87 старый, действительно старый. Это замена SSE . SSE вычисляет 32-битные и 64-битные операции, что приводит к минимальной прецессии, потерянной по математике. Обратите внимание, что проблемы прецессии с плавающей точкой все еще существуют, но printf("%f\n", cosf(M_PI_2)); должно быть равно нулю. Черт возьми, даже сравнение с плавающей точкой с SSE снова точно! (в отличие от x87).

Так как последний Xcode на самом деле является GCC 4.2.1, используйте переключатель компилятора -msse3 -mfpmath=sse и посмотрите, как вы получите идеально округленный 0.00000 (Примечание: если вы получаете -0.00000, не беспокойтесь, это совершенно нормально и до сих пор равно 0.00000 в соответствии со спецификацией IEEE (подробнее читайте в этой статье wikipedia )).

Для всех компьютеров Intel гарантировано с поддержкой SSE3 (за исключением Mac OSx86, если вы хотите их поддерживать, используйте -msse2).

...