簡體   English   中英

為什么這個使用 gcc、-mfpmath=387 和優化級別 -O2 或 -O3 編譯的簡單程序會產生 NaN 值?

[英]Why does this simple program compiled with gcc,-mfpmath=387, and an optimization level of -O2 or -O3 produce NaN values?

我有一個執行數值計算的短程序,當某些特定條件成立時,會得到不正確的 NaN 結果。 我看不出這個 NaN 結果是如何產生的。 請注意,我沒有使用允許重新排序算術運算的編譯器選項,例如-ffath-math

問題:我正在尋找 NaN 結果如何產生的解釋。 在數學上,計算中沒有任何東西會導致除以零或類似的東西。 我錯過了一些明顯的東西嗎?

請注意,我不是在問如何解決問題——這很容易。 我只是想了解 NaN 是如何出現的。

最小的例子

請注意,此示例非常脆弱,即使是很小的修改(例如在循環中添加printf()調用以觀察值)也會改變行為。 這就是為什么我無法進一步最小化它的原因。

// prog.c

#include <stdio.h>
#include <math.h>

typedef long long myint;

void fun(const myint n, double *result) {
    double z = -1.0;
    double phi = 0.0;
    for (myint i = 0; i < n; i++) {
        double r = sqrt(1 - z*z);

        /* avoids division by zero when r == 0 */
        if (i != 0 && i != n-1) {
            phi += 1.0 / r;
        }

        double x = r*cos(phi);
        double y = r*sin(phi);

        result[i + n*0] = x;
        result[i + n*1] = y;
        result[i + n*2] = z;

        z += 2.0 / (n - 1);
    }
}

#define N 11

int main(void) {
    // perform computation
    double res[3*N];
    fun(N, res);

    // output result
    for (int i=0; i < N; i++) {
        printf("%g %g %g\n", res[i+N*0], res[i+N*1], res[i+N*2]);
    }

    return 0;
}

編譯:

gcc -O3 -mfpmath=387 prog.c -o prog -lm

output 的最后一行是:

nan nan 1

我希望數字接近於零,而不是 NaN。

示例的關鍵特征

以下必須全部成立才能出現 NaN output:

  • 在 x86 平台上用 GCC 編譯。 我能夠在 macOS 10.14.6 上使用 GCC 12.2.0(來自 MacPorts)進行重現,在 Linux(openSUSE Leap 15.3)上使用 GCC 版本 9.3.0、8.3.0 和 7.5.0 進行重現。

    無法在 Linux 上使用 GCC 10.2.0 或更高版本,或者在 macOS 上使用 GCC 11.3.0 重現它。

  • 選擇使用帶有-mfpmath=387的 x87 指令,以及-O2-O3的優化級別。

  • myint必須是帶符號的 64 位類型。

  • result視為 n×3 矩陣,它必須按列優先順序存儲。

  • fun()的主循環中沒有printf()調用。

沒有這些功能,我確實得到了預期的 output,即最后一行類似於1.77993e-08 -1.12816e-08 10 0 1

程序說明

盡管這對問題來說並不重要,但我還是對程序的作用做了一個簡短的解釋,以使其更容易理解。 它以特定排列計算球體表面n個點的xyz三維坐標。 z值 go 從 -1 到 1 以相等的增量,但是,由於數值舍入誤差,最后一個值不會恰好為 1。 坐標被寫入一個n × 3 矩陣result ,以列優先順序存儲。 rphi是 (x, y) 平面中的極坐標。

請注意,當z-11時, r變為 0。這發生在第一個和最后一個迭代步驟中。 這將導致在1.0 / r表達式中除以 0。 但是, 1.0 / r被排除在循環的第一次和最后一次迭代之外。

這是由 x87 80 位內部精度的相互作用、GCC 的不一致行為以及編譯器版本之間的優化決策不同引起的。

x87 僅支持 IEEE binary32 和 binary64 作為存儲格式,在加載/存儲時與其 80 位表示形式相互轉換。 為了使程序行為可預測,C 標准要求在賦值時放棄額外精度,並允許通過FLT_EVAL_METHOD宏檢查中間精度。 使用-mfpmath=387時, FLT_EVAL_METHOD為 2,因此您知道中間精度對應於long double類型。

不幸的是, GCC 不會降低分配的額外精度,除非您通過-std=cNN (而不是-std=gnuNN )請求更嚴格的一致性,或者明確傳遞-fexcess-precision=standard

在你的程序中, z += 2.0 / (n - 1); 聲明應通過以下方式計算:

  1. 以中間 80 位精度計算2.0 / (n - 1)
  2. 添加到之前的z值(仍然是 80 位精度)。
  3. 四舍五入到z的聲明類型(即到 binary64)

在以 NaN 結尾的版本中,GCC 改為執行以下操作:

  1. 在循環之前計算2.0 / (n - 1)一次。
  2. 將該分數從 binary80 舍入為 binary64 並存儲在堆棧中。
  3. 在循環中,它從堆棧重新加載此值並添加到z

這是不符合要求的,因為2.0 / (n - 1)進行了兩次舍入(首先是 binary80,然后是 binary64)。


上面解釋了為什么您看到不同的結果取決於編譯器版本和優化級別。 但是,通常您不能期望您的計算在最后一次迭代中不產生 NaN。 n - 1不是 2 的冪時, 2.0 / (n - 1)不能精確表示,可能會四舍五入。 在這種情況下,“z”的增長速度可能比真正的和-1.0 + 2.0 / (n - 1) * i快一點,並且對於i == n - 1可能最終超過 1.0,導致sqrt(1 - z*z)由於參數是否定而產生 NaN。

事實上,如果您在程序中將#define N 11更改為#define N 12 ,您將確定性地獲得具有 80 位和 64 位中間精度的 NaN。

... NaN 結果是如何產生的(?)

盡管更好地遵守 C 規范顯然可以解決 OP 的直接問題,但我斷言應該考慮其他預防措施。


|z| > 1.0時, sqrt(1 - z*z)是候選 NaN |z| > 1.0

除以零的索引測試預防可能還不夠,然后導致cos(INFINITE) ,這是另一種 NaN 可能性。

// /* avoids division by zero when r == 0 */
//    if (i != 0 && i != n-1) {
//        phi += 1.0 / r;
//    }

為了避免這些,1) 直接測試和 2) 使用更精確的方法。

if (r) {
  phi += 1.0 / r;
}

// double r = sqrt(1 - z*z);
double rr = (1-z)*(1+z);  // More precise than 1 - z*z
double r = rr < 0.0 ? 0.0 : sqrt(rr);

暫無
暫無

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

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