Диагностика переполнений с плавающей запятой в программах на 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