Диагностика переполнений с плавающей запятой в программах на C++
у меня есть ситуация, в которой некоторые численные результаты (включая арифметику с плавающей запятой с double
и float
) становятся некорректными для больших размеров, но не для маленьких.
В общем, я хотел бы знать, какие инструменты доступны для диагностики таких условий, как численные переполнения и проблемная потеря точности.
другими словами: есть инструмент, который жалуется на переполнение и т. д. так же, как валгринд жалуется на ошибки в памяти?
4 ответов
Если вы включаете исключения с плавающей запятой, то FPU может вызвать исключение при переполнении. Как именно это работает, зависит от операционной системы. Например:
- в Windows вы можете использовать _control87см чтобы разоблачить _EM_OVERFLOW, чтобы вы получили исключение C++ при переполнении.
- в Linux вы можете использовать feenableexcept чтобы включить исключения на FE_OVERFLOW, чтобы вы получили сигнала sigfpe при переполнении. Например, включить все исключения, называют
feenableexcept(FE_ALL_EXCEPT)
в своемmain
. Чтобы включить переполнение и деление на ноль, вызовитеfeenableexcept(FE_OVERFLOW | FE_DIVBYZERO)
.
обратите внимание, что во всех случаях, сторонний код может отключить исключения, которые вы используете; это, вероятно, редкая на практике.
Это, вероятно, не так хорошо, как Valgrind, так как это больше похоже на отладчик и ручную проверку, чем на получение приятного резюме в конце, но это работает.
для диагностики переполнения можно использовать исключения с плавающей запятой. См., например,cppreference. Обратите внимание, что для настройки поведения ошибок с плавающей запятой может потребоваться использование функций, специфичных для реализации.
обратите внимание, что, хотя они часто называются "исключениями", ошибки с плавающей запятой не вызывают исключений c++.
код cppreference показывает, каким должно быть поведение по умолчанию для реализаций на основе IEEE 754: вы можете проверить флаги исключений с плавающей запятой, когда вы находите это уместным. Вы должны очистить флаги для ввода вашего расчета. Вы можете подождать, пока ваш расчет не закончится, чтобы увидеть, если он установил какие-либо флаги или вы можете проверить каждую операцию, которую вы подозреваете в подверженности ошибке.
могут быть расширения, специфичные для реализации, чтобы такие "исключения" вызывали то, что вы не можете игнорировать. В Windows / MSVC++ это может быть "структурированное исключение" (не настоящий C++), в Linux, который может быть SIGFPE (поэтому вам нужен обработчик сигналов для обработки ошибок). Для включения такого поведения вам понадобятся функции библиотеки, специфичные для реализации, или даже флаги компилятора/компоновщика.
Я бы все равно предположил, что переполнение вряд ли будет вашей проблемой. Если некоторые из ваших входных данных становятся большими, а другие остаются маленькими, вы, вероятно, потеряете точность при их объединении. Один из способов управления-использовать интервальную арифметику. Существуют различные библиотеки для это, включая увеличить интервал.
отказ от ответственности: у меня нет опыта работы с этой библиотекой (или другими интервальными арифметическими библиотеками), но, возможно, это может помочь вам начать.
в дополнение к отличным предложениям, уже опубликованным, вот еще один подход. Напишите функцию, которая проверяет структуры данных с плавающей запятой, выполняет проверку диапазона и согласованности. Вставьте вызовы в основной цикл. Чтобы изучить другие переменные, вы можете установить точку останова в checker после того, как он нашел проблему.
Это больше настройка работы, чем включение исключений, но может подобрать более тонкие проблемы, такие как несоответствия и числа, которые больше, чем ожидается, не пройдя бесконечность, что приведет к обнаружению, близкому к исходной проблеме.
Возможно, вам нужно отладить реализацию алгоритма, в котором вы, возможно, сделали ошибку кодирования и хотите отслеживать выполняемые вычисления с плавающей запятой. Возможно, Вам нужен крюк, чтобы проверить все значения, которые работают, ища значения, которые кажутся вне диапазона, который вы ожидаете. В C++ вы можете определить свой собственный floating point
класс и перегружать оператора пользы написать ваши вычисления в естественном путе, пока сохраняющ способность проверить все проведенные расчеты.
например, вот программа, которая определяет FP
class, и печатает все дополнения и умножения.
#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 xffffffc0,%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 xffffffc0,%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