浮点异常

IEEE浮点数标准定义了六种异常,每种错误都对应于特定类型的错误。当异常发生时(在标准语言中,当异常被引发时),可能发生以下两种情况之一。默认情况下,只需在浮点状态字中记录异常,程序将继续运行,就好像什么都没有发生一样。该操作生成一个默认值,该值取决于异常。您的程序可以检查状态字以找出发生了哪些异常。或者,您可以为异常启用陷阱。在这种情况下,当引发异常时,您的程序将收到SIGFPE信号。此信号的默认操作是终止程序。当检测到某些异常时,发出异常信号。通常会引发(设置)这些异常的标志,并传递默认结果,然后继续执行。这种默认行为通常是可取的,尤其是在应用上线运行时,但在开发过程中,当发出异常信号时,暂停会很有用。停止异常就像在程序中的每个浮点操作中添加一个断言,因此,这是提高代码可靠性的一个很好的方法,可以从根本上找到神秘的行为。

让我们重新认识IEEE浮点标准规定的六个例外是:

  1. 无效操作(EM_INVALID)如果给定的操作数对于要执行的操作无效(有或没有可用的可定义结果),则引发此异常。示例包括(参见IEEE 754第7节):例如零除以零、无穷减无穷、0乘无穷、无穷除无穷、余数:x REM y,其中y为零或x为无穷大、sqrt(-1),当浮点数无法以目标格式表示时(由于溢出、无穷大或NaN)、将浮点数转换为整数或十进制字符串、无法识别的输入字符串的转换则发出此信号。默认结果为NaN(不是数字)
  2. 被零除(EM_ZERODIVIDE):非零数字被零除时发出信号。结果是无穷大。
  3. 溢出(EM_OVERFLOW):如果结果不能以目标的精度格式表示为有限值或当四舍五入结果不合适时,会发出此信号。默认结果是无穷大。每当引发溢出异常时,也会引发不精确异常。
  4. 下溢(EM_UNDERFLOW):如果中间结果太小而无法准确计算,或者如果四舍五入到目标精度的操作结果太小而无法标准化,则会引发下溢异常。又或当结果为非零且介于-FLT_MIN和FLT_MIN之间时发出信号。默认结果为四舍五入结果。
  5. 不精确(EM_INEXACT):任何时候操作结果不精确时都会发出此信号。默认结果是四舍五入的结果。
  6. 非规格化(EM_DENORMAL):非规范异常(仅适用于控制87)

开发者通常对下溢异常不感兴趣,因为它很少发生,而且通常不会检测到任何感兴趣的东西。不精确的结果通常也不会引起开发人员的兴趣-它经常发生(虽然不总是,并且理解什么操作是精确的可能很有用),但通常不会检测到任何感兴趣的东西。无效操作、除零和溢出在开发的环境中通常是非常特殊的。它们很少是故意做的,所以它们通常表示一个bug。在许多情况下,这些错误是良性的,但偶尔这些错误表明真正的问题。从现在起,我将把前三个异常称为“坏”异常,并假设开发人员希望避免它们。被零除什么时候有用?虽然“坏”异常通常表示程序上下文中的无效操作,但并非在所有上下文中都是如此。被零除的默认结果(无穷大)可允许继续计算并生成有效结果,无效操作的默认结果(NaN)有时可允许使用快速算法,如果生成NaN结果,则使用较慢且更稳健的算法。零除行为的经典示例是并联电阻的计算。对于电阻为R1和R2的两个电阻器,其计算公式为:

 

 

因为被零除得到无穷大的结果,因为无穷大加上另一个数得到无穷大,因为被无穷大除的有限数得到零,所以当R1或R2为零时,此计算出正确的零并联电阻。如果没有这种行为,代码将需要检查R1和R2是否为零,并专门处理这种情况。此外,如果R1或R2非常小–小于FLT_MAX或DBL_MAX的倒数,则此计算结果将为零。此零结果在技术上不正确。如果程序员需要区分这些场景,则需要监控溢出和零除标志。假设我们没有试图利用除零行为,那么抵抗是徒劳的。我们需要一种方便的方法来打开“坏”浮点异常。而且,由于我们必须与其他代码共存(调用物理库、D3D和其他可能不“异常清除”的代码),因此我们还需要一种临时关闭所有浮点异常的方法。实现这一点的适当方法是使用一对类,它们的构造函数和析构函数发挥了必要的魔力。下面是一些用于VC++的类:

// Declare an object of this type in a scope in order to suppress
// all floating-point exceptions temporarily. The old exception
// state will be reset at the end.
class FPExceptionDisabler
{
public:
    FPExceptionDisabler()
    {
        // Retrieve the current state of the exception flags. This
        // must be done before changing them. _MCW_EM is a bit
        // mask representing all available exception masks.
        _controlfp_s(&mOldValues, _MCW_EM, _MCW_EM);
        // Set all of the exception flags, which suppresses FP
        // exceptions on the x87 and SSE units.
        _controlfp_s(0, _MCW_EM, _MCW_EM);
    }
    ~FPExceptionDisabler()
    {
        // Clear any pending FP exceptions. This must be done
        // prior to enabling FP exceptions since otherwise there
        // may be a 'deferred crash' as soon the exceptions are
        // enabled.
        _clearfp();

        // Reset (possibly enabling) the exception status.
        _controlfp_s(0, mOldValues, _MCW_EM);
    }

private:
    unsigned int mOldValues;

    // Make the copy constructor and assignment operator private
    // and unimplemented to prohibit copying.
    FPExceptionDisabler(const FPExceptionDisabler&);
    FPExceptionDisabler& operator=(const FPExceptionDisabler&);
};

// Declare an object of this type in a scope in order to enable a
// specified set of floating-point exceptions temporarily. The old
// exception state will be reset at the end.
// This class can be nested.
class FPExceptionEnabler
{
public:
    // Overflow, divide-by-zero, and invalid-operation are the FP
    // exceptions most frequently associated with bugs.
    FPExceptionEnabler(unsigned int enableBits = _EM_OVERFLOW |
                _EM_ZERODIVIDE | _EM_INVALID)
    {
        // Retrieve the current state of the exception flags. This
        // must be done before changing them. _MCW_EM is a bit
        // mask representing all available exception masks.
        _controlfp_s(&mOldValues, _MCW_EM, _MCW_EM);

        // Make sure no non-exception flags have been specified,
        // to avoid accidental changing of rounding modes, etc.
        enableBits &= _MCW_EM;

        // Clear any pending FP exceptions. This must be done
        // prior to enabling FP exceptions since otherwise there
        // may be a 'deferred crash' as soon the exceptions are
        // enabled.
        _clearfp();

        // Zero out the specified bits, leaving other bits alone.
        _controlfp_s(0, ~enableBits, enableBits);
    }
    ~FPExceptionEnabler()
    {
        // Reset the exception state.
        _controlfp_s(0, mOldValues, _MCW_EM);
    }

private:
    unsigned int mOldValues;

    // Make the copy constructor and assignment operator private
    // and unimplemented to prohibit copying.
    FPExceptionEnabler(const FPExceptionEnabler&);
    FPExceptionEnabler& operator=(const FPExceptionEnabler&);
};

代码里的注释解释了很多细节,但我在这里也会提到一些_controlfp_s是旧_control87函数便携版的安全版本。_controlfp_s控制x87和SSE FPU的异常设置。它还可用于控制两个FPU上的舍入方向,在x87 FPU上,它可用于控制精度设置。这些类使用mask参数来确保仅更改异常设置。浮点异常标志是粘性的,因此当异常标志被提升时,它将保持设置,直到显式清除为止。这意味着,如果您选择不启用浮点异常,您仍然可以检测是否发生了任何异常。如果在引发标志后启用了与标志相关的异常,则下一条FPU指令将触发异常,即使是在引发标志的操作后数个周期。因此,每次启用异常之前清除异常标志至关重要。典型用法浮点异常标志是处理器状态的一部分,这意味着它们是每个线程都需要设置。因此,如果希望在任何地方启用异常,则需要在每个线程(通常在main/WinMain和线程启动函数中)中启用异常,方法是在这些函数的顶部删除FPExceptionEnabler对象。当调用D3D或任何可能以触发这些异常的方式使用浮点的代码时,您需要放入FPExceptionDisabler对象。或者,如果代码大部分都不是FP异常干净的,那么在大多数情况下禁用FP异常,然后在特定区域(如粒子系统)启用它们可能更有意义。由于更改异常状态会带来一些成本(FPU管道至少会被刷新),并且由于使代码更粗糙可能不是您想要的,因此您应该在构造函数和析构函数中添加#ifdef。过去曾出现过各种情况,这些情况会启用浮点异常并使其保持启用状态,这意味着一些完全合法的软件在调用第三方代码(例如打印后)后会开始崩溃。在您的代码中调用函数后,有人不幸的崩溃是一种可怕的经历,因此,如果您的代码最终可能被注入其他进程,请特别小心。在这种情况下,返回时绝对不需要启用浮点异常,并且可能需要容忍在启用浮点异常的情况下被调用。引发异常标志(触发浮点异常)的异常不应有性能影响。这些标志的提升频率足够高,任何CPU设计者都可以确保这样做是免费的。例如,几乎每个浮点指令上都会出现不精确标志。但是,启用异常可能会很昂贵。在超标量CPU上交付精确异常可能是一项挑战,一些CPU选择在启用浮点异常时禁用FPU并行来实现这一点。这会影响性能。当启用任何浮点异常时,Xbox 360 CPU中使用的PowerPC CPU(可能是PS3中使用的CPU)会显著降低速度。这意味着,在这些处理器上使用此技术时,您应该根据需要启用FPU异常。下面的示例代码调用TryDivByZero()三次–一次在默认环境中,一次在启用三个“坏”浮点异常的情况下,一次在再次抑制它们的情况下。TryDivByZero在Win32 _try/__except块内执行浮点除零操作,以捕获异常、打印消息并允许测试继续。这种类型的结构化异常处理块不应用于生产代码中,除非可能用于记录崩溃然后退出。我不太愿意演示这种技术,因为我担心它会被误用。在意外的结构化异常之后继续是完全邪恶的。话虽如此,代码如下:

int __cdecl DescribeException(PEXCEPTION_POINTERS pData, const char *pFunction)
{
    // Clear the exception or else every FP instruction will
    // trigger it again.
    _clearfp();

    DWORD exceptionCode = pData->ExceptionRecord->ExceptionCode;
    const char* pDescription = NULL;
    switch (exceptionCode)
    {
    case STATUS_FLOAT_INVALID_OPERATION:
        pDescription = "float invalid operation";
        break;
    case STATUS_FLOAT_DIVIDE_BY_ZERO:
        pDescription = "float divide by zero";
        break;
    case STATUS_FLOAT_OVERFLOW:
        pDescription = "float overflow";
        break;
    case STATUS_FLOAT_UNDERFLOW:
        pDescription = "float underflow";
        break;
    case STATUS_FLOAT_INEXACT_RESULT:
        pDescription = "float inexact result";
        break;
    case STATUS_FLOAT_MULTIPLE_TRAPS:
        // This seems to occur with SSE code.
        pDescription = "float multiple traps";
        break;
    default:
        pDescription = "unknown exception";
        break;
    }

    void* pErrorOffset = 0;
#if defined(_M_IX86)
    void* pIP = (void*)pData->ContextRecord->Eip;
    pErrorOffset = (void*)pData->ContextRecord->FloatSave.ErrorOffset;
#elif defined(_M_X64)
    void* pIP = (void*)pData->ContextRecord->Rip;
#else
    #error Unknown processor
#endif

    printf("Crash with exception %x (%s) in %s at %p!n",
            exceptionCode, pDescription, pFunction, pIP);

    if (pErrorOffset)
    {
        // Float exceptions may be reported in a delayed manner -- report the
        // actual instruction as well.
        printf("Faulting instruction may actually be at %p.n", pErrorOffset);
    }

    // Return this value to execute the __except block and continue as if
    // all was fine, which is a terrible idea in shipping code.
    return EXCEPTION_EXECUTE_HANDLER;
    // Return this value to let the normal exception handling process
    // continue after printing diagnostics/saving crash dumps/etc.
    //return EXCEPTION_CONTINUE_SEARCH;
}

static float g_zero = 0;

void TryDivByZero()
{
    __try
    {
        float inf = 1.0f / g_zero;
        printf("No crash encountered, we successfully calculated %f.n", inf);
    }
    __except(DescribeException(GetExceptionInformation(), __FUNCTION__))
    {
        // Do nothing here - DescribeException() has already done
        // everything that is needed.
    }
}

int main(int argc, char* argv[])
{
#if _M_IX86_FP == 0
    const char* pArch = "with the default FPU architecture";
#elif _M_IX86_FP == 1
    const char* pArch = "/arch:sse";
#elif _M_IX86_FP == 2
    const char* pArch = "/arch:sse2";
#else
#error Unknown FP architecture
#endif
    printf("Code is compiled for %d bits, %s.n", sizeof(void*) * 8, pArch);

    // Do an initial divide-by-zero.
    // In the registers window if display of Floating Point
    // is enabled then the STAT register will have 4 ORed
    // into it, and the floating-point section's EIP register
    // will be set to the address of the instruction after
    // the fdiv.
    printf("nDo a divide-by-zero in the default mode.n");
    TryDivByZero();
    {
        // Now enable the default set of exceptions. If the
        // enabler object doesn't call _clearfp() then we
        // will crash at this point.
        FPException

 

posted on 2021-10-13 08:20  活着的虫子  阅读(1840)  评论(0编辑  收藏  举报

导航