Обработка исключений с плавающей запятой в С++

Я считаю, что проблемы с моделью/ошибкой с плавающей запятой довольно запутанны. Это область, с которой я не знаком, и я не программист с низким уровнем C/asm, поэтому я был бы признателен за немного советов.

У меня есть довольно большое приложение на С++, построенное с помощью VS2012 (VC11), которое я настроил на то, чтобы вызывать исключения с плавающей запятой (точнее, чтобы среда выполнения С++ и/или аппаратное обеспечение выдавали исключение fp) - и это бросание довольно много в выпуске (оптимизированной) сборке, но не в сборке отладки. Я предполагаю, что это связано с оптимизацией и, возможно, с плавающей точкой (хотя компилятор /fp: точный переключатель установлен как для релизов, так и для отладочных сборников).

Мой первый вопрос касается управления отладкой приложения. Я хочу контролировать, где выбрасываются исключения fp и где они "замаскированы". Это необходимо, потому что я отлаживаю (оптимизированную) сборку релиза (где происходят исключения fp) - и я хочу отключить исключения fp в определенных функциях, где я обнаружил проблемы, поэтому я могу найти новые проблемы с FP. Но меня смущает разница между использованием _controlfp_s для этого (что прекрасно работает) и коммутатором компилятора (и #pragma float_control) "/fp: except" (что, кажется, не имеет эффекта). В чем разница между этими двумя механизмами? Предполагают ли они, что они имеют одинаковый эффект на исключениях fp?

Во-вторых, я получаю несколько исключений с проверкой стека с плавающей запятой, в том числе тот, который, похоже, вызывается при вызове GDI + dll. Поиск в Интернете, несколько упоминаний об этом исключении, похоже, указывают на то, что это связано с ошибками компилятора. Это вообще так? Если да, то как мне обойти это? Лучше ли отключать оптимизацию компилятора для проблемных функций или отключать исключения fp только для проблемных областей кода, если не отображаются какие-либо плохие значения с плавающей запятой? Например, в вызове GDI + (в GraphicsPath:: GetPointCount), который генерирует это исключение, фактическое возвращаемое целочисленное значение кажется правильным. В настоящее время я использую _controlfp_s для отключения fp-исключений непосредственно перед вызовом GDI +, а затем снова использовать его для повторного включения исключений непосредственно после вызова.

Наконец, мое приложение действительно делает много вычислений с плавающей запятой и должно быть надежным и надежным, но не обязательно очень точным. Характер приложения заключается в том, что значения с плавающей запятой обычно указывают вероятности, поэтому по своей сути несколько неточны. Тем не менее, я хочу уловить любые чистые логические ошибки, такие как деление на ноль. Какая лучшая модель fp для этого? В настоящее время я:

  • захват всех исключений fp (т.е. EM_OVERFLOW | EM_UNDERFLOW | EM_ZERODIVIDE | EM_DENORMAL | EM_INVALID) с использованием _controlfp_s и обработчика сигналов SIGFPE,
  • включили денормалы-ноль (DAZ) и flush-to-zero (FTZ) (т.е. _MM_SET_FLUSH_ZERO_MODE (_MM_DENORMALS_ZERO_ON)) и
  • Я использую настройки компилятора VC11 по умолчанию /fp: точный с /fp: кроме не указанных.

Является ли это лучшей моделью?

Спасибо и приветствую!

Ответ 1

Большая часть следующей информации поступает из сообщения Брюса Доусона по этой теме (ссылка).

Поскольку вы работаете с С++, вы можете создать класс RAII, который включает или отключает исключения с плавающей запятой в области действия. Это позволяет вам иметь больший контроль, чтобы вы только отображали состояние исключения для своего кода, а не вручную управляли вызовом _controlfp_s() самостоятельно. Кроме того, состояние исключения с плавающей запятой, которое установлено таким образом, является системным, поэтому рекомендуется помнить предыдущее состояние управляющего слова и восстанавливать его, когда это необходимо. RAII может позаботиться об этом для вас и является хорошим решением для проблем с GDI +, которые вы описываете.

Флаги исключений _EM_OVERFLOW, _EM_ZERODIVIDE и _EM_INVALID являются наиболее важными для учета. _EM_OVERFLOW возникает, когда положительная или отрицательная бесконечность является результатом вычисления, тогда как _EM_INVALID возникает, когда результатом является сигнализация NaN. _EM_UNDERFLOW безопасно игнорировать; он сигнализирует, когда результат вычисления отличен от нуля и между -FLT_MIN и FLT_MIN (другими словами, когда вы генерируете денормаль). _EM_INEXACT возникает слишком часто, чтобы быть практическим использованием из-за характера арифметики с плавающей запятой, хотя она может быть информативной, если пытаться выявить неточные результаты в некоторых ситуациях.

SIMD-код добавляет больше морщин в микс; так как вы не укажете явно использование SIMD, я не буду обсуждать это, кроме как отметить, что указание чего-либо другого, кроме /fp: fast, может отключить автоматическую векторизацию вашего кода в VS 2012; см. этот ответ для получения подробной информации об этом.

Ответ 2

Я не могу много помочь с первыми двумя вопросами, но у меня есть опыт и предложение по поводу маскировки исключений FPU.

Я нашел функции

_statusfp()  (x64 and Win32)
_statusfp2() (Win32 only)
_fpreset()
_controlfp_s()
_clearfp()
_matherr()

полезно при отладке исключений FPU и в предоставлении стабильного и быстрого продукта.

При отладке я выборочно размаскиваю исключения, чтобы изолировать строку кода, где в вычислении генерируется исключение fpu, где я не могу избежать вызова другого кода, который непредсказуемо генерирует исключения fpu (например, разделение .NET JIT на нули).

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

Я маскирую все исключения FPU, когда мне приходится вызывать код, который нельзя изменить, не имеет надежной обработки исключений и иногда генерирует исключения FPU.

Пример:

#define BAD_FPU_EX (_EM_OVERFLOW | _EM_ZERODIVIDE | _EM_INVALID)
#define COMMON_FPU_EX (_EM_INEXACT | _EM_UNDERFLOW | _EM_DENORMAL)
#define ALL_FPU_EX (BAD_FPU_EX | COMMON_FPU_EX)

Код выпуска:

_fpreset();
Use _controlfp_s() to mask ALL_FPU_EX 
_clearfp();
... calculation
unsigned int bad_fpu_ex = (BAD_FPU_EX  & _statusfp());
_clearfp(); // to prevent reacting to existing status flags again
if ( 0 != bad_fpu_ex )
{
  ... use fallback calculation
  ... discard result and return error code
  ... throw exception with useful information
}

Код отладки

_fpreset();
_clearfp();
Use _controlfp_s() to mask COMMON_FPU_EX and unmask BAD_FPU_EX 
... calculation
  "crash" in debugger on the line of code that is generating the "bad" exception.

В зависимости от ваших параметров компилятора в сборках релизов могут использоваться встроенные вызовы для операций FPU, а отладочные сборки могут вызывать функции математической библиотеки. Эти два метода могут иметь существенно различное поведение обработки ошибок для недопустимых операций, таких как sqrt (-1.0).

Используя исполняемые файлы, созданные с помощью VS2010 в 64-разрядной версии Windows 7, я генерировал несколько разные арифметические значения с двойной точностью при использовании идентичного кода на платформах Win32 и x64. Даже используя не оптимизированные отладочные сборки с /fp:: exact, управление точностью fpu явно задано _PC_53, а управление округлением fpu явно установлено на _RC_NEAR. Мне пришлось настроить некоторые регрессионные тесты, которые сравнивают значения двойной точности, чтобы учитывать платформу. Я не знаю, если это все еще проблема с VS2012, но хедз-ап.

Ответ 3

Я пытаюсь получить некоторую информацию об обработке исключений с плавающей запятой в Linux, и я могу рассказать вам, что я узнал: Существует несколько способов включения механизма исключения:

  • fesetenv (FE_NOMASK_ENV); разрешает все исключения
  • feenableexcept (FE_ALL_EXCEPT);
 fpu_control_t fw;
 _FPU_GETCW(fw);
 fw |=FE_ALL_EXCEPT;
 _FPU_SETCW(fw);

4.

> fenv_t envp; include bits/fenv.h   
> fegetenv(&envp);    
 envp.__control_word |= ~_FPU_MASK_OM;   
> fesetenv(&envp);

5.

> fpu_control_t cw;
> __asm__ ("fnstcw %0" : "=m" (*&cw));get config word
>cw |= ~FE_UNDERFLOW;
> __asm__ ("fldcw %0" : : "m" (*&cw));write config word

Режим 6.С++: std:: feclearexcept (FE_ALL_EXCEPT);

Есть несколько полезных ссылок: http://frs.web.cern.ch/frs/Source/MAC_headers/fpu_control.h http://en.cppreference.com/w/cpp/numeric/fenv/fetestexcept http://technopark02.blogspot.ro/2005/10/handling-sigfpe.html