Несоответствие вычисления синуса в Visual C++ 2012?

рассмотрим следующий код:

// Filename fputest.cpp

#include <cmath>
#include <cstdio>

int main()
{
    double x;
    *(__int64 *) &x = 0xc01448ec3aaa278di64; // -5.0712136427263319
    double sine1 = sin(x);
    printf("%016llXn", sine1);
    double sine2;
    __asm {
    fld x
    fsin
    fstp sine2
    }
    printf("%016llXn", sine2);
    return 0;
}

при компиляции с Visual C++ 2012 (cl fputest.cpp) и программа выполняется, вывод следующий:

3FEDF640D8D36174
3FEDF640D8D36175

вопросы:

  • почему эти два разных значения?
  • можно ли выдать некоторые параметры компилятора, чтобы вычисленные значения синуса были точно такими же?

4 ответов


эта проблема не вызвана преобразованием из long double в double. Это может быть связано с неточностью в sin режим в математической библиотеке.

на fsin указывается инструкция для получения результата в пределах 1 ULP (в длинном двойном формате) для операндов в пределах его диапазона (на Intel 64 и IA-32 Architectures Software Developer's Manual, октябрь 2011, Том 1, 8.3.10), в режиме "от раунда до ближайшего". На Intel Core i7,fsin значения спрашивающего, -5.07121364272633190495298549649305641651153564453125 или-0x1.448ec3aaa278dp+2, производит 0xe.fb206c69b0ba402p-4. Мы можем легко видеть из этого шестнадцатеричного, что последние 11 бит 100 0000 0010. Это биты, которые будут округлены при преобразовании из long double. Если они больше 100 0000 0000, число будет округлено. Они больше. Поэтому результатом преобразования этого длинного двойного значения в double является 0xe.fb206c69b0ba8p-4, что равно 0x1.df640d8d36175p-1 и 0.93631021832247418590355891865328885614871978759765625. Также обратите внимание, что даже если результат был на один ULP ниже, последние 11 бит все равно будут больше 100 0000 0000 и все равно округлятся. Поэтому этот результат не должен отличаться от процессоров Intel, соответствующих приведенной выше документации.

сравните это с вычислением синуса двойной точности напрямую, используя идеальный sin процедура, которая дает правильно округленные результаты. Синус значения приблизительно 0.93631021832247413051857150785044253634581268961333520518023697738674775240815140702992025520721336793516756640679315765619707343171517531053811196321335899848286682535203710849065933755262347468763562 (вычислено с кленом 10). Самый близкий к этому двойник-0x1.df640d8d36175p-1. Это то же значение, которое мы получили при преобразовании fsin результат удвоить.

таким образом, расхождение не вызвано преобразованием long double в double; преобразование long double fsin результат удвоения дает точно такой же результат, как идеальная двойная точность sin рутины.

у нас нет спецификации для точности sin процедура, используемая пакетом Visual Studio задающего вопрос. В коммерческих библиотеках, позволяя ошибки 1 ULP или нескольких ULP является распространенным явлением. Обратите внимание, насколько близок синус к точке, где значение двойной точности округлено: это так .498864 ULP (double-precision ULP) далеко от двойника, так оно и есть .001136 ОТП от точка, где округление изменяется. Поэтому даже очень небольшая неточность в sin процедура заставит его вернуть 0x1.df640d8d36174p-1 вместо ближе 0х1.df640d8d36175p-1.

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


(Примечание: как упоминалось в комментариях, это не работает на VC2012. Я оставил его здесь для общей информации. Я бы не рекомендовал полагаться ни на что, что зависит от уровня оптимизации в любом случае!)

У меня нет VS2012, но в компиляторе VS2010 вы можете указать /fp:fast в командной строке, а затем я получаю те же результаты. Это заставляет компилятор генерировать "быстрый" код, который не обязательно полностью соответствует требуемому округлению и правилам в C++, но который соответствует ваш расчет языка сборки.

Я не могу попробовать это в VS2012, но я думаю, что у него есть тот же вариант.

это работает только в оптимизированной сборке тоже с /Ox как вариант.


посмотреть почему cos (x) != cos (y), хотя x == y?

As Дэвид упоминается в комментарии, несоответствие происходит от перемещения данных в регистре FP в место памяти (регистр/ОЗУ) другого размера. И это не всегда назначение; даже другой близлежащей операции с плавающей запятой может быть достаточно, чтобы очистить регистр FP, что делает любую попытку гарантировать определенное значение бесполезной. Если вам нужно сделать сравнение, можно чтобы смягчить некоторые из этого, принудительно все результаты в расположение памяти следующим образом:

float F1 = sin(a); float F2 = sin(b); if (F1 == F2)

однако, даже это может не сработать. Лучший подход-признать, что любая операция с плавающей запятой будет только "в основном точной" и что с точки зрения программистов эта ошибка будет фактически непредсказуемой и случайной, даже если одна и та же операция выполняется неоднократно. Вместо

if (F1 == F2)

вы должны использовать что-то для эффекта из

if (isArbitrarilyClose(F1, F2))

или

if (absf(F1 - F2) <= n)

здесь n - это ничтожное количество.


генератор кода в VS2012 был существенно изменен на support автоматическая векторизация. Часть этого изменения заключается в том, что x86 floating point math теперь выполняется в SSE2 и больше не использует FPU, необходимый, потому что код FPU не может быть векторизован. SSE2 вычисляет с 64-битной точностью вместо 80-битной точности, что дает хорошие шансы на то, что результаты будут отключены на один бит из-за округления. Также причина, по которой @J99 может получить согласованный результат с /fp: fast в VS2010, его компилятором по-прежнему использует FPU и /fp:fast использует результат FSIN напрямую.

в этой функции много взрыва, проверьте видео Джима Хогга по связанному url-адресу, чтобы узнать, как использовать его.