浮點異常


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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM