[英]Why does returning a floating-point value change its value?
以下代碼在Red Hat 5.4 32位上引發assert
,但在Red Hat 5.4 64位(或CentOS)上工作。
在32位上,我必須將返回值millis2seconds
放入變量中,否則將引發assert
,這表明從函數返回的double
值與傳遞給它的值不同。
如果您在“ #define BUG”行中添加注釋,它將起作用。
感謝@R,將-msse2 -mfpmath選項傳遞給編譯器使millis2seconds函數的兩個變體都可以工作。
/*
* TestDouble.cpp
*/
#include <assert.h>
#include <stdint.h>
#include <stdio.h>
static double millis2seconds(int millis) {
#define BUG
#ifdef BUG
// following is not working on 32 bits architectures for any values of millis
// on 64 bits architecture, it works
return (double)(millis) / 1000.0;
#else
// on 32 bits architectures, we must do the operation in 2 steps ?!? ...
// 1- compute a result in a local variable, and 2- return the local variable
// why? somebody can explains?
double result = (double)(millis) / 1000.0;
return result;
#endif
}
static void testMillis2seconds() {
int millis = 10;
double seconds = millis2seconds(millis);
printf("millis : %d\n", millis);
printf("seconds : %f\n", seconds);
printf("millis2seconds(millis) : %f\n", millis2seconds(millis));
printf("seconds < millis2seconds(millis) : %d\n", seconds < millis2seconds(millis));
printf("seconds > millis2seconds(millis) : %d\n", seconds > millis2seconds(millis));
printf("seconds == millis2seconds(millis) : %d\n", seconds == millis2seconds(millis));
assert(seconds == millis2seconds(millis));
}
extern int main(int argc, char **argv) {
testMillis2seconds();
}
使用Linux x86系統上使用的cdecl調用約定,使用st0 x87寄存器的函數將返回一個double。 所有x87寄存器均為80位精度。 使用此代碼:
static double millis2seconds(int millis) {
return (double)(millis) / 1000.0;
};
編譯器使用80位精度計算除法。 當gcc使用標准的GNU方言(默認情況下會執行該操作)時,它將結果保留在st0寄存器中,因此會將全精度返回給調用方。 匯編代碼的末尾如下所示:
fdivrp %st, %st(1) # Divide st0 by st1 and store the result in st0
leave
ret # Return
有了這段代碼,
static double millis2seconds(int millis) {
double result = (double)(millis) / 1000.0;
return result;
}
結果存儲到64位存儲位置,這會降低精度。 在返回之前,將64位值重新加載到80位st0寄存器中,但是損壞已經完成:
fdivrp %st, %st(1) # Divide st0 by st1 and store the result in st0
fstpl -8(%ebp) # Store st0 onto the stack
fldl -8(%ebp) # Load st0 back from the stack
leave
ret # Return
在您的主機中,第一個結果存儲在64位內存位置中,因此,兩種方式都會失去額外的精度:
double seconds = millis2seconds(millis);
但是在第二次調用中,直接使用返回值,因此編譯器可以將其保存在寄存器中:
assert(seconds == millis2seconds(millis));
當使用第一個版本的millis2seconds
,您最終將已被截斷為64位精度的值與具有完整80位精度的值進行比較,因此存在差異。
在x86-64上,使用SSE寄存器(只有64位)完成計算,因此不會出現此問題。
另外,如果使用-std=c99
以便不獲取GNU方言,則計算所得的值將存儲在內存中,並在返回之前重新加載到寄存器中,以使其符合標准。
在i386(32位x86)上,所有浮點表達式都被評估為80位IEEE擴展的浮點類型。 這反映在float.h的FLT_EVAL_METHOD
,定義為2。將結果存儲到變量或對結果進行FLT_EVAL_METHOD
會通過舍入降低過多的精度,但是仍然不足以保證您將看到的結果相同。一個沒有過多精度的實現(例如x86_64),因為與在同一步驟中執行計算和舍入相比,兩次舍入可以得出不同的結果。
解決此問題的一種方法是甚至在x86目標上也使用-msse2 -mfpmath=sse
來構建SSE數學。
首先值得注意的是,由於該函數是隱式的純函數,並使用一個常量參數對其進行了兩次調用,因此編譯器將有權完全取消計算和比較。
clang-3.0-6ubuntu3確實使用-O9消除了純函數調用,並且在編譯時執行了所有浮點計算,因此程序成功了。
C99標准ISO / IEC 9899表示
浮點操作數的值和浮點表達式的結果可以比類型所需的精度和范圍大。 類型不會因此改變。
因此,正如其他人所描述的,編譯器可以自由地傳回80位值。 但是,該標准繼續說:
仍然需要強制轉換和賦值運算符執行其指定的轉換。
這就解釋了為什么專門為double
賦值會強制將值降低到64位,而從函數返回double
卻不會。 這讓我感到非常驚訝。
但是,看起來C11標准實際上將通過添加以下文本來減少混淆:
如果返回表達式是用不同於返回類型的浮點格式求值的,則該表達式的轉換就好像是通過將函數的返回類型賦值[刪除了任何多余的范圍和精度]一樣,結果值返回到呼叫者。
因此,此代碼基本上在未確定的行為上執行該值在各個點是否被截斷的操作。
對我來說,在Ubuntu Precise上,使用-m32
:
clang
傳 clang -O9
也通過 gcc
,斷言失敗 gcc -O9
通過,因為它也消除了常量表達式 gcc -std=c99
失敗 gcc -std=c1x
也會失敗(但可能會在以后的gcc上運行) gcc -ffloat-store
通過,但似乎具有不斷消除的副作用 我認為這不是gcc錯誤,因為標准允許這種行為,但是clang行為更好。
除了在其他答案中解釋的所有詳細信息之外,我想說的是關於Fortran以來幾乎所有編程語言中使用浮點類型的非常簡單的規則: 切勿檢查浮點值是否精確相等 。 關於80位和64位值的所有知識都是對的,但對於某些硬件和某個編譯器,則是對的(是的,如果您更改編譯器,甚至打開或關閉優化,則可能會有所改變)。 更通用的規則(適用於任何旨在移植的代碼 )是,浮點值通常不像整數或字節序列,並且可以更改(例如,在復制時),並且檢查它們的相等性通常會帶來不可預測的結果。
因此,即使它在測試中起作用,通常也最好不要這樣做。 某些更改之后,它可能會失敗。
UPD:盡管有些人對此表示反對,但我堅持建議通常是正確的。 似乎只是在復制值的東西(從高級編程語言的程序員的角度來看,它們看起來是這樣;在最初的示例中發生的是一個典型的示例,該值被返回並放入變量中-瞧-它已更改!),可以更改浮點值。 比較相等或不相等的浮點值通常是一個壞習慣,只有在您知道為什么在特定情況下可以這樣做時,才允許這樣做。 編寫可移植程序通常需要最小化底層知識。 是的,當將整數值(例如0或1)放入浮點變量或進行復制時,更改的可能性很小。 但是可能會有更復雜的值(在上面的示例中,我們看到了簡單算術表達式的結果會發生什么!)。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.