簡體   English   中英

如何避免單元測試中的浮點舍入錯誤?

[英]How to avoid floating point round off error in unit tests?

我正在嘗試為一些在單精度浮點數數組上運行的簡單矢量數學函數編寫單元測試。 這些函數使用SSE內在函數,並且在32位系統(測試通過64位)上運行時,我得到了誤報(至少在我看來)。 隨着操作遍歷數組,我累積了越來越多的舍入錯誤。 這是單元測試代碼和輸出的摘要(以下是我的實際問題):

測試設置:

static const int N = 1024;
static const float MSCALAR = 42.42f;

static void setup(void) {
    input = _mm_malloc(sizeof(*input) * N, 16);
    ainput = _mm_malloc(sizeof(*ainput) * N, 16);
    output = _mm_malloc(sizeof(*output) * N, 16);
    expected = _mm_malloc(sizeof(*expected) * N, 16);

    memset(output, 0, sizeof(*output) * N);

    for (int i = 0; i < N; i++) {
        input[i] = i * 0.4f;
        ainput[i] = i * 2.1f;
        expected[i] = (input[i] * MSCALAR) + ainput[i];
    }
}

然后我的主要測試代碼調用要測試的功能(這確實用於產生相同的計算expected陣列)和檢查其對輸出expected上方產生數組。 該檢查是針對緊密度(0.0001內)而不是相等性。

樣本輸出:

0.000000    0.000000    delta: 0.000000
44.419998   44.419998   delta: 0.000000
...snip 100 or so lines...
2043.319946 2043.319946 delta: 0.000000
2087.739746 2087.739990 delta: 0.000244
...snip 100 or so lines...
4086.639893 4086.639893 delta: 0.000000
4131.059570 4131.060059 delta: 0.000488
4175.479492 4175.479980 delta: 0.000488
...etc, etc...

我知道我有兩個問題:

  1. 在32位計算機上,387和SSE浮點算術單元之間的差異。 我相信387將更多位用於中間值。
  2. 我用來生成期望值的42.42值的非精確表示。

所以我的問題是,為浮點數據的數學運算編寫有意義可移植的單元測試的正確方法是什么?

*通過便攜式,我的意思是應該同時傳遞32位和64位體系結構。

通過評論,我們看到正在測試的功能本質上是:

for (int i = 0; i < N; ++i)
    D[i] = A[i] * b + C[i];

其中A[i]bC[i]D[i]都具有float類型。 當引用單個迭代的數據時,我將對A[i]C[i]D[i]使用acd

下面是對測試此功能時可以用於容錯的分析。 不過,我首先要指出的是,我們可以設計測試,以確保沒有錯誤。 我們可以選擇A[i]bC[i]D[i]以便最終結果和中間結果的所有結果都可以精確表示,並且沒有舍入誤差。 顯然,這不會測試浮點運算,但這不是目標。 目的是測試該功能的代碼:它是否執行計算所需功能的指令? 只需選擇將揭示使用正確數據,添加,乘法或存儲到正確位置失敗的任何值,就足以揭示函數中的錯誤。 我們相信硬件可以正確執行浮點運算,並且不會對其進行測試; 我們只想測試函數是否正確編寫。 為此,我們可以例如將b設置為2的冪, A[i]為各種小整數,將C[i]為各種小整數乘以b 如果需要,我可以更精確地詳細說明這些值的限制。 這樣,所有結果將是精確的,並且在比較中允許公差的任何需求都將消失。

除此之外,讓我們繼續進行錯誤分析。

目的是發現功能實現中的錯誤。 為此,我們可以忽略浮點運算中的小錯誤,因為我們正在尋找的各種錯誤幾乎總是導致大錯誤:使用錯誤的操作,使用錯誤的數據或結果沒有存儲在所需位置,因此實際結果幾乎總是與預期結果有很大差異。

現在的問題是,我們應該容忍多少錯誤? 由於錯誤通常會導致較大的錯誤,因此我們可以將公差設置得很高。 但是,在浮點數中,“高”仍然是相對的。 與數萬億的值相比,一百萬的誤差很小,但是當輸入值在數萬億的值中時,發現誤差就太大了。 因此,我們應該至少進行一些分析來確定級別。

被測試的函數將使用SSE內部函數。 這意味着,對於上面循環中的每個i ,它將執行浮點乘法和浮點加法,或者將執行融合的浮點乘法加法。 后者中的潛在錯誤是前者的一個子集,因此我將使用前者。 a*b+c的浮點運算會進行四舍五入,以便計算出近似a•b + c的結果(解釋為精確的數學表達式,而不是浮點)。 對於所有誤差最大為2 -24的誤差e0和e1,我們可以將計算出的精確值寫為(a•b•(1+e0)+c)•(1+e1) ,前提是所有值都在正常范圍內浮點格式。 (2 -24是在IEEE-754 32位二進制浮點格式下,在四舍五入模式下正確舍入的基本浮點運算中可能發生的最大相對誤差。將數學值最多更改為有效數中最低有效位的值的一半,該值比最高有效位低23位。)

接下來,我們考慮測試程序為其預期值產生的值。 它使用C代碼d = a*b + c; (我已將問題中的長名稱轉換為短名稱。)理想情況下,這還將計算IEEE-754 32位二進制浮點中的乘和加。 如果確實如此,那么結果將與被測試的功能相同,並且不需要在比較中留出任何公差。 但是,C標准允許實現在執行浮點算術時具有一定的靈活性,並且有些不符合標准的實現比標准允許的自由度更高。

常見的行為是,比標稱類型更精確地計算表達式。 一些編譯器可能使用doublelong double算術來計算a*b + c C標准要求在轉換或分配時將結果轉換為名義類型; 必須放棄額外的精度。 如果C實現使用的是額外的精度,則計算將繼續: a*b的計算精度更高,因為a和b的doublelong double具有足夠的精度以精確表示任意兩個float值的乘積,所以得出a•b。 然后,AC實施可能會將結果四舍五入為float 這不太可能,但無論如何我都允許。 但是,我也忽略了它,因為它使預期結果更接近被測試函數的結果,並且我們只需要知道可能發生的最大錯誤即可。 因此,在更糟(更遙遠)的情況下,我將繼續說,到目前為止的結果是a•b。 然后將c相加,得到(a•b + c)•(1 + e2),其中某個e2的大小最大為2 -53 (64位二進制格式的正常數的最大相對誤差)。 最后,將該值轉換為float以分配給d ,對於某些e3,其大小最大為2 -24產生(a•b + c)•(1 + e2)•(1 + e3)。

現在,我們得到了由正確的運算函數(a•b•(1 + e0)+ c)•(1 + e1)計算出的精確結果的表達式,以及由測試代碼計算的(a•b + c)•(1 + e2)•(1 + e3),我們可以計算出它們可以相差多少的界限。 簡單的代數告訴我們確切的差是a•b•(e0 + e1 + e0•e1-e2-e3-e2•e3)+ c•(e1-e2-e3-e2•e3)。 這是e0,e1,e2和e3的簡單函數,我們可以看到其極端情況出現在e0,e1,e2和e3的電勢值的端點處。 由於值的符號的可能性之間存在相互作用,因此會帶來一些復雜性,但是在最壞的情況下,我們可以簡單地允許一些額外的錯誤。 差異最大幅度的界為| a•b |•(3•2 -24 +2 -53 +2 -48 )+ | c |•(2•2 -24 +2 -53 +2 -77 )。

因為我們有足夠的空間,所以我們可以簡化它,只要我們朝着增大值的方向進行即可。 例如,使用| a•b |•3.001•2 -24 + | c |•2.001•2 -24可能會很方便。 該表達式應足以在檢測幾乎所有實現錯誤的同時舍入浮點計算。

請注意,表達式與最終值a*b+c不成正比,最終值是由所測試的函數或測試程序計算得出的。 這通常意味着使用相對於被測函數或測試程序計算出的最終值的公差的測試是錯誤的。 測試的正確形式應如下所示:

double tolerance = fabs(input[i] * MSCALAR) * 0x3.001p-24 + fabs(ainput[i]) * 0x2.001p-24;
double difference = fabs(output[i] - expected[i]);
if (! (difference < tolerance))
   // Report error here.

總而言之,這給我們帶來的容忍度要大於由於浮點舍入而產生的任何可能的差異,因此,它絕對不能給我們帶來誤報(報告測試功能未損壞時會報告)。 但是,與由我們要檢測的錯誤引起的錯誤相比,它很小,因此它很少會給我們帶來假陰性(無法報告實際錯誤)。

(請注意,還有一些計算公差的舍入誤差,但是它們小於我在系數中使用.001時所允許的斜率,因此我們可以忽略它們。)

(還要注意! (difference < tolerance)不等於difference >= tolerance 。如果該函數由於錯誤而產生NaN,則任何比較都將得出false: difference < tolerancedifference >= tolerance得出false,但是! (difference < tolerance)得出true。)

在32位計算機上,387和SSE浮點算術單元之間的差異。 我相信387將更多位用於中間值。

如果您將GCC用作32位編譯器,則可以使用-msse2 -mfpmath=sse選項告訴它仍生成SSE2代碼。 可以告訴Clang使用兩個選項之一執行相同的操作,而忽略另一個選項(我忘了哪個)。 在這兩種情況下,二進制程序都應實現嚴格的IEEE 754語義,並計算與64位程序相同的結果,該程序也使用SSE2指令來實現嚴格的IEEE 754語義。

我用來生成期望值的42.42值的非精確表示。

C標准規定,必須將諸如42.42f的文字轉換為以十進制表示的數字的正上方或正下方的浮點數。 此外,如果文字可以精確表示為預期格式的浮點數,則必須使用此值。 但是,質量編譯器(例如GCC)將為您提供*( 最接近的可表示浮點數),其中只有一個,因此,只要您使用質量編譯器,這也不是真正的可移植性問題(或至少是同一編譯器)。

如果發現這是個問題,一種解決方案是編寫您想要的常數的精確表示。 這樣的精確表示形式在十進制格式中可能會非常長(對於double的精確表示形式,最多為750個十進制數字),但在C99的十六進制格式中始終非常緊湊: 0x1.535c28p+5用於最接近42.42的float的精確表示形式。 用於C程序的靜態分析平台的最新版本Frama-C可以通過選項-warn-decimal-float:all 提供所有不精確的十進制浮點常量的十六進制表示。


(*)排除了一些舊版GCC中的轉換錯誤。 有關詳細信息,請參見Rick Regan的博客

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM