64-битные программы и вычисления с плавающей точкой

Андрей Карпов
Статей: 375



К нам в поддержку обратился разработчик, занимающийся портированием своего Windows-приложения на 64-битную платформу. Он задал вопрос, связанный с использованием вычислений с плавающей точкой. С его разрешения мы публикуем в блоге ответ на вопрос, поскольку эта тема может быть интересна и другим разработчикам.

Текст письма

Хочу задать вам один конкретный вопрос, касающийся миграции 32 -> 64 бита. Статьи и материалы на вашем сайте я изучал, тем более был удивлён тому несоответствию в работе 32- и 64-битного кода, что я обнаружил.

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

float fConst = 1.4318620f; 
float fValue1 = 40.598053f * (1.f - 1.4318620f / 100.f); 
float fValue2 = 40.598053f * (1.f - fConst / 100.f); 

MSVC 32, SSE и SSE2 отключены

/fp:precise: fValue1 = 40.016743, fValue2 = 40.016747 

MSVC 64, SSE и SSE2 отключены

/fp:precise: fValue1 = 40.016743, fValue2 = 40.016743 

Проблема в том, что отличаются значения fValue2. Из-за этого несоответствия код, скомпилированный под 32 и под 64 бита, даёт разные результаты, что недопустимо в моём случае (да и, наверное, недопустимо вообще).

Находит ли что-то похожее ваш продукт? Не могли бы вы дать наводку, как 32/64 может влиять на то, что выдаёт вещественная арифметика?

Наш ответ

Продукт Viva64 не выявляет подобные изменения в поведении в программы после ее перекомпиляции для 64-битной системы. Подобные изменения нельзя назвать ошибочными. Давайте разберемся с описанной ситуацией подробней.

Простое объяснение

Взглянем для начала на то, что выдает 32-битный компилятор: fValue1 = 40.016743, fValue2 = 40.016747.

Вспомним, что тип float имеет 7 значащих цифр. Отсюда видно, что на самом деле мы получаем значение, которое чуть больше 40.01674 (семь значащих цифр). Будет ли это на самом деле 40.016743 или 40.016747 не имеет значения, поскольку это за пределами точности типа float.

При компиляции в 64-битном режиме компилятор генерирует такой же корректный код, в результате которого мы получаем то же самое значение "чуть больше 40.01674". В данном случае это всегда 40.016743. Но это не имеет значения. В рамках точности типа float мы получаем такой же результат, как и в 32-битной программе.

Еще раз - результат вычислений на 32-битной и 64-битной системе одинаков в рамках возможностей типа float.

Более строгое объяснение

Точностью типа float является значение FLT_EPSILON, равное 0.0000001192092896.

Если мы прибавим к 1.0f значение меньше чем FLT_EPSILON, то получим вновь 1.0f. Только прибавление к 1.0f значения равного или большего FLT_EPSILON увеличит значение переменной: 1.0f + FLT_EPSILON !=1.0f.

В нашем случае мы работаем не с единицей, а со значениями 40.016743, 40.016747. Возьмем максимальное из них и умножим на FLT_EPSILON. Полученное число будет значением точности для наших вычислений:

Epsilon = 40.016743*FLT_EPSILON = 40.016743*0.0000001192092896 = 0,0000047703675051357728

Посмотрим, насколько различаются числа 40.016747 и 40.016743:

Delta = 40.016747 - 40.016743 = 0.000004

Оказывается, что разница меньше, чем погрешность:

Delta < Epsilon

0.000004 < 0,00000477

Следовательно, 40.016743 == 40.016747 в рамках типа float.

Как поступить?

Хотя все корректно, от этого, к сожалению часто не легче. Если есть желание сделать систему более детерминированной, то можно использовать ключ /fp:strict.

В этом случае результат работы будет следующий:

MSVC x86:

/fp:strict: fValue1 = 40.016747, fValue2 = 40.016747

MSVC x86-64:

/fp:strict: fValue1 = 40.016743, fValue2 = 40.016743

Результат стал более стабильный, но мы опять не достигли идентичного поведения 32-битного и 64-битного кода. Что делать? Только смириться и изменить методику сравнения результатов.

Не знаю, насколько то, что я опишу, совпадает с вашей ситуацией, но мне кажется это что-то близкое.

Я занимался разработкой пакета численного моделирования. Была поставлена задача, разработать систему регрессионных тестов. Есть набор проектов, результат которых просмотрен физиками и оценен как корректный. Правки кода, вносимые в проект не должны приводить к тому, чтобы выходные данные начали отличаться. Если в какой-то точке в момент t давление 5 атмосфер, то это давление должно остаться в ней и после добавления новой кнопки в диалоге или оптимизации механизма начального заполнения области. Если что-то меняется, то значит, были правки в модели и физики должны заново оценить все изменения. Естественно предполагается, что подобные правки модели крайне редкая ситуация. В нормальном режиме разработки проекта должны получаться идентичные данные. Однако это теоретически. На практике все сложнее. Идентичный результат не всегда можно было получить, работая даже с одним компилятором с одинаковыми ключами оптимизации. Результаты все равно очень легко начинали "плыть". Но поскольку проект еще и собирался различными компиляторами под различные платформы, то получить совершенно идентичные значения было признано не решаемой задачей. Вернее возможно задача и решаемая, но это требует огромное количество усилий и приведет к недопустимому падению скорости вычислений из-за невозможности оптимизаций кода. Решением стала специальная система сравнения результатов. Причем значения в различных точках сравнивались не просто с точностью Epsilon, а специальным образом. Подробности реализации я уже не помню, но идея была следующая. Если в области протекают процессы, в результате которой максимум давления составляет 10 атмосфер, то в другой точке, разница в 0.001 атмосферу считается ошибкой. Однако если протекает процесс, где образуются участки с давлением 1000 атмосфер, то разница в 0.001 уже считается допустимой погрешностью. Таким образом, удалось построить достаточно надежную систему регрессионного тестирования, которая, думается, с успехом работает и поныне.

Последний момент, а почему все-таки мы получаем разный результат в 32-битном и 64-битном коде?

Видимо дело в том, что используется разный набор инструкций. В 64-битном режиме теперь всегда используются SSE2 инструкции, которые реализованы во всех процессорах семейства AMD64 (Intel 64). Кстати, поэтому в исходном вопросе фраза "MSVC 64, SSE и SSE2 отключены" является неверной. SSE2 используется 64-битным компилятором в любом случае.

Дополнительные ресурсы



Найдите ошибки в своем C, C++, C# и Java коде

Предлагаем попробовать проверить код вашего проекта с помощью анализатора кода PVS-Studio. Одна найденная в нём ошибка скажет вам о пользе методологии статического анализа кода больше, чем десяток статей.

goto PVS-Studio;

Андрей Карпов
Статей: 375


Найденные ошибки

Проверено проектов
361
Собрано ошибок
13 417

А ты совершаешь ошибки в коде?

Проверь с помощью
PVS-Studio

Статический анализ
кода для C, C++, C#
и Java

goto PVS-Studio;