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 через соответствующие флаги управляющего слова - в : /* 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 эти опции используются по умолчанию.