[英]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 1
或0 0 1
。
盡管這對問題來說並不重要,但我還是對程序的作用做了一個簡短的解釋,以使其更容易理解。 它以特定排列計算球體表面n
個點的x
、 y
、 z
三維坐標。 z
值 go 從 -1 到 1 以相等的增量,但是,由於數值舍入誤差,最后一個值不會恰好為 1。 坐標被寫入一個n
× 3 矩陣result
,以列優先順序存儲。 r
和phi
是 (x, y) 平面中的極坐標。
請注意,當z
為-1
或1
時, 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);
聲明應通過以下方式計算:
2.0 / (n - 1)
。z
值(仍然是 80 位精度)。z
的聲明類型(即到 binary64) 。在以 NaN 結尾的版本中,GCC 改為執行以下操作:
2.0 / (n - 1)
一次。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.