[Comm] g++ и неточности с плавающей точкой

Sergey Vlasov =?iso-8859-1?q?vsu_=CE=C1_altlinux=2Eru?=
Вт Апр 8 15:00:55 MSD 2008


On Tue, Apr 08, 2008 at 01:30:31AM +0700, Michael Pozhidaev wrote:
> Вопрос к знатокам глубин работы g++. Во время отладки порядочно
> нагромождённой программы на C++ обратил внимание на то, что результаты
> вычислений с плавающей точкой отличаются в зависимости от параметров
> оптимизации и отладки.  То, что получается при -g, отличается от того,
> что получается при -O2.  Вопрос в том, результат ли это недоглядок,
> вроде использования неинициализированных переменных или такое поведение
> нормально.  Можно ли добиться, чтобы вычисления всегда проходили
> стабильно или g++ явно генерирует различный код, приводящий к разным
> результатам даже на корректной программе?

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

В архитектуре x86 регистры сопроцессора i387, используемые при
операциях с плавающей точкой, имеют разрядность 80 бит (что
соответствует типу long double).  Если производятся вычисления с типом
double, компилятор в зависимости от опций оптимизации может не
сохранять промежуточные значения в памяти (что привело бы к их
округлению до точности, обеспечиваемой 64-разрядным типом double), а
брать их из регистров сопроцессора (с сохранением 80-разрядной
точности); возможно, как раз это и является причиной изменения
результатов при смене флагов оптимизации.  У gcc есть опция
-ffloat-store, включение которой заставляет gcc сохранять все
промежуточные результаты вычислений с плавающей точкой в памяти, что
устраняет избыточную точность, но приводит к замедлению работы кода
из-за большого количества лишних команд пересылки между регистрами
сопроцессора и памятью.

Конечно, можно использовать для всех переменных тип long double, чтобы
все расчёты производились с максимально возможной точностью независимо
от того, будет ли компилятор сохранять промежуточные результаты в
памяти.  Однако такой код может менее эффективно работать на x86_64,
где по умолчанию операции с плавающей точкой выполняются через SSE2
(команды SSE2 поддерживают только float и double, а вычисления с long
double могут выполняться только с использованием команд i387).  Кроме
того, текущая версия valgrind не поддерживает эмуляцию точности long
double для i387 - при запуске под valgrind все операции с плавающей
точкой выполняются с точностью типа double, в результате поведение
программы может измениться.

Также есть возможность ограничить точность операций i387 через
соответствующие флаги управляющего слова - в <fpu_control.h>:

/* precision control */
#define _FPU_EXTENDED 0x300     /* libm requires double extended precision.  */
#define _FPU_DOUBLE   0x200
#define _FPU_SINGLE   0x0

Однако, судя по комментариям в этом файле, при включении такого
округления функции из библиотеки libm могут работать неправильно
(впрочем, под valgrind они как-то работают - возможно, в некоторых
случаях точность окончательного результата типа double будет ниже, чем
должна была бы получиться при 80-разрядных промежуточных переменных).

Наконец, если допустимо требование поддержки процессором команд SSE2,
можно собирать код с опциями -msse2 -mfpmath=sse - в этом случае
вычисления с типами float и double будут выполняться с помощью команд
SSE и SSE2, не использующих 80-разрядную точность.  На x86_64 эти
опции используются по умолчанию.
----------- следующая часть -----------
Было удалено вложение не в текстовом формате...
Имя     : =?iso-8859-1?q?=CF=D4=D3=D5=D4=D3=D4=D7=D5=C5=D4?=
Тип     : application/pgp-signature
Размер  : 189 байтов
Описание: Digital signature
Url     : <http://lists.altlinux.org/pipermail/community/attachments/20080408/abfddbb3/attachment.bin>


Подробная информация о списке рассылки community