Диагностика переполнения с плавающей запятой в программах на С++
У меня есть ситуация, когда некоторые числовые результаты (с использованием арифметики с плавающей запятой с double
и float
) становятся неправильными для больших размеров ввода, но не для небольших.
В общем, я хотел бы знать, какие инструменты доступны для диагностики таких состояний, как числовые переполнения и проблемная потеря точности.
Другими словами: существует ли инструмент, который жалуется на переполнение и т.д. так же, как valgrind жалуется на ошибки памяти?
Ответы
Ответ 1
Если вы включаете исключения с плавающей запятой, то FPU может генерировать исключение при переполнении. Как именно это работает, зависит от операционной системы. Например:
- В Windows вы можете использовать _ control87, чтобы разоблачить _EM_OVERFLOW, чтобы вы получили исключение С++ при переполнении.
- В Linux вы можете использовать feenableexcept, чтобы включить исключения в FE_OVERFLOW, чтобы вы получили SIGFPE при переполнении. Например, чтобы включить все исключения, вызовите
feenableexcept(FE_ALL_EXCEPT)
в main
. Чтобы включить переполнение и делить на ноль, вызовите feenableexcept(FE_OVERFLOW | FE_DIVBYZERO)
.
Обратите внимание, что во всех случаях сторонний код может отключать исключения, которые вы включили; это, вероятно, редко на практике.
Это, вероятно, не так хорошо, как Valgrind, поскольку он больше проверяет "отладчик-отладчик и-вручную", чем "get-a-nice-summary-at-the-end", но он работает.
Ответ 2
Чтобы диагностировать переполнение, вы можете использовать исключения с плавающей запятой. См. Например cppreference. Обратите внимание, что для настройки поведения ошибок с плавающей запятой вам может потребоваться использование специальных функций для реализации.
Обратите внимание, что, хотя их часто называют "исключениями", ошибки с плавающей запятой не вызывают исключения С++.
Код cppreference показывает, каково должно быть поведение по умолчанию для реализаций на основе IEEE 754: вы можете проверять флаги исключений с плавающей запятой всякий раз, когда вы сочтете это подходящим. Вы должны очистить флаги перед внесением ваших расчетов. Вы можете подождать до тех пор, пока ваш расчет не пройдет, если он установил какие-либо флаги, или вы можете проверить каждую операцию, которая, как вы подозреваете, подвержена ошибкам.
Возможно, существуют расширения, специфичные для реализации, для запуска такого "исключения", что вы не можете игнорировать. В Windows/MSVС++, который может быть "структурированным исключением" (а не реальным С++), в Linux, который может быть SIGFPE (для этого вам нужен обработчик сигналов для обработки ошибок). Для обеспечения такого поведения вам понадобятся функции библиотеки конкретных реализаций или даже флаги компилятора/компоновщика.
Я бы все же предположил, что переполнение вряд ли будет вашей проблемой. Если некоторые из ваших входных данных становятся большими, а другие значения остаются малыми, вы, вероятно, потеряете точность при их объединении. Один из способов контроля - использовать интервальную арифметику. Для этого существуют различные библиотеки, включая интервал обновления.
Отказ от ответственности: у меня нет опыта работы с этой библиотекой (а также с другими арифметическими библиотеками интервалов), но, возможно, это может помочь вам начать.
Ответ 3
Возможно, вам нужно отладить реализацию алгоритма, в котором вы, возможно, допустили ошибку кодирования и хотите отслеживать выполняемые вычисления с плавающей запятой. Возможно, вам нужен крючок, чтобы проверить все используемые значения, ища значения, которые, как представляется, находятся вне диапазона, который вы ожидаете. В С++ вы можете определить свой собственный класс floating point
и использовать перегрузку оператора, чтобы естественным образом писать ваши вычисления, сохраняя при этом возможность проверять все вычисления.
Например, здесь программа, которая определяет класс FP
, и выводит все дополнения и умножения.
#include <iostream>
struct FP {
double value;
FP( double value ) : value(value) {}
};
std::ostream & operator<< ( std::ostream &o, const FP &x ) { o << x.value; return o; }
FP operator+( const FP & lhs, const FP & rhs ) {
FP sum( lhs.value + rhs.value );
std::cout << "lhs=" << lhs.value << " rhs=" << rhs.value << " sum=" << sum << std::endl;
return sum;
}
FP operator*( const FP & lhs, const FP & rhs ) {
FP product( lhs.value * rhs.value );
std::cout << "lhs=" << lhs.value << " rhs=" << rhs.value << " product=" << product << std::endl;
return product;
}
int main() {
FP x = 2.0;
FP y = 3.0;
std::cout << "answer=" << x + 2 * y << std::endl;
return 0;
}
Какая печать
lhs=2 rhs=3 product=6
lhs=2 rhs=6 sum=8
answer=8
Обновление: Я улучшил программу (на x86), чтобы отобразить флаги состояния с плавающей запятой после каждой операции с плавающей запятой (только реализовано добавление и умножение, другие могут быть легко добавлены).
#include <iostream>
struct MXCSR {
unsigned value;
enum Flags {
IE = 0, // Invalid Operation Flag
DE = 1, // Denormal Flag
ZE = 2, // Divide By Zero Flag
OE = 3, // Overflow Flag
UE = 4, // Underflow Flag
PE = 5, // Precision Flag
};
};
std::ostream & operator<< ( std::ostream &o, const MXCSR &x ) {
if (x.value & (1<<MXCSR::IE)) o << " Invalid";
if (x.value & (1<<MXCSR::DE)) o << " Denormal";
if (x.value & (1<<MXCSR::ZE)) o << " Divide-by-Zero";
if (x.value & (1<<MXCSR::OE)) o << " Overflow";
if (x.value & (1<<MXCSR::UE)) o << " Underflow";
if (x.value & (1<<MXCSR::PE)) o << " Precision";
return o;
}
struct FP {
double value;
FP( double value ) : value(value) {}
};
std::ostream & operator<< ( std::ostream &o, const FP &x ) { o << x.value; return o; }
FP operator+( const FP & lhs, const FP & rhs ) {
FP sum( lhs.value );
MXCSR mxcsr, new_mxcsr;
asm ( "movsd %0, %%xmm0 \n\t"
"addsd %3, %%xmm0 \n\t"
"movsd %%xmm0, %0 \n\t"
"stmxcsr %1 \n\t"
"stmxcsr %2 \n\t"
"andl $0xffffffc0,%2 \n\t"
"ldmxcsr %2 \n\t"
: "=m" (sum.value), "=m" (mxcsr.value), "=m" (new_mxcsr.value)
: "m" (rhs.value)
: "xmm0", "cc" );
std::cout << "lhs=" << lhs.value
<< " rhs=" << rhs.value
<< " sum=" << sum
<< mxcsr
<< std::endl;
return sum;
}
FP operator*( const FP & lhs, const FP & rhs ) {
FP product( lhs.value );
MXCSR mxcsr, new_mxcsr;
asm ( "movsd %0, %%xmm0 \n\t"
"mulsd %3, %%xmm0 \n\t"
"movsd %%xmm0, %0 \n\t"
"stmxcsr %1 \n\t"
"stmxcsr %2 \n\t"
"andl $0xffffffc0,%2 \n\t"
"ldmxcsr %2 \n\t"
: "=m" (product.value), "=m" (mxcsr.value), "=m" (new_mxcsr.value)
: "m" (rhs.value)
: "xmm0", "cc" );
std::cout << "lhs=" << lhs.value
<< " rhs=" << rhs.value
<< " product=" << product
<< mxcsr
<< std::endl;
return product;
}
int main() {
FP x = 2.0;
FP y = 3.9;
std::cout << "answer=" << x + 2.1 * y << std::endl;
std::cout << "answer=" << x + 2 * x << std::endl;
FP z = 1;
for( int i=0; i<310; ++i) {
std::cout << "i=" << i << " z=" << z << std::endl;
z = 10 * z;
}
return 0;
}
Последний цикл умножает число на 10
достаточно, чтобы показать переполнение. Вы также заметите ошибки точности. Он заканчивается тем, что значение будет бесконечным после его переполнения.
Здесь хвост выхода
lhs=10 rhs=1e+305 product=1e+306 Precision
i=306 z=1e+306
lhs=10 rhs=1e+306 product=1e+307
i=307 z=1e+307
lhs=10 rhs=1e+307 product=1e+308 Precision
i=308 z=1e+308
lhs=10 rhs=1e+308 product=inf Overflow Precision
i=309 z=inf
lhs=10 rhs=inf product=inf
Ответ 4
В дополнение к отличным предложениям, уже опубликованным, вот еще один подход. Напишите функцию, которая исследует ваши структуры данных с плавающей точкой, выполняя проверки диапазона и согласованности. Вставьте вызовы в свой основной цикл. Чтобы проверить другие переменные, вы можете установить контрольную точку в контролере после того, как она обнаружит проблему.
Это более эффективная работа, чем включение исключений, но может подобрать более тонкие проблемы, такие как несогласованности и числа, которые больше, чем ожидалось, без бесконечности, что приведет к обнаружению, близкому к исходной проблеме.