關於 C++ 浮點數計算誤差問題?
int main(int argc, char* argv[])
{
int a = 68280;int result = static_cast&
(a * 0.3);
printf("%d
", result);double result1 = a * 0.3;
printf("%f
", result1);float result2 = static_cast&
(a * 0.3);
printf("%f
", result2);int result3 = static_cast&
(68280 * 0.3);
printf("%d
", result3);int result4 = static_cast&
(a * 0.3f);
printf("%d
", result4);
}
gcc4.9.2 (32位) 編譯的程序運行結果是:
為什麼只第一種情況會有誤差?
20483
20484.000000
20484.000000
20484
20484
浮點數0.3的確是會比整數0.3少一點,所以,我們產生彙編文件的話,我們會看到類似這樣的話
.LCPI0_0:
.quad 4599075939470750515 # double 0.29999999999999999
.text
.globl main
.align 16, 0x90
.type main,@function
然而,若我們使用clang或者上面一位知友說的Visual C++是不會產生20483結果的,而是20484。
而且你使用gcc使用-m64編譯這段程序,或者在Mac下面使用-m32編譯,都會出現20484,而不是20483,唯獨在Linux下面使用-m32會出現這樣的結果。
那麼讓我們看gcc -m32的彙編文件,不過我這裡把程序代碼精簡了一下#include &
int main(int argc, char *argv[]) {
int a = 68280;
int result = a * 0.3;
printf("%d
", result);
}
那麼產生的彙編文件是
main:
.LFB0:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
andl $-16, %esp
subl $32, %esp
movl $68280, 24(%esp)
fildl 24(%esp)
fldl .LC0
fmulp %st, %st(1)
fnstcw 14(%esp)
movzwl 14(%esp), %eax
movb $12, %ah
movw %ax, 12(%esp)
fldcw 12(%esp)
fistpl 28(%esp)
fldcw 14(%esp)
movl 28(%esp), %eax
movl %eax, 4(%esp)
movl $.LC1, (%esp)
call printf
一切都是我們想像的進行,a 從int 轉為 double , 進行相乘等等,所以你要得到20484, 你需要進行round等操作。
那麼,為什麼clang / VC++, gcc -m64, Mac下面的gcc -m32/-m64可以得到20484呢?
讓我們看看它們的彙編文件main: # @main
# BB#0:
pushl %ebp
movl %esp, %ebp
subl $40, %esp
movl 12(%ebp), %eax
movl 8(%ebp), %ecx
leal .L.str, %edx
movsd .LCPI0_0, %xmm0
movl %ecx, -4(%ebp)
movl %eax, -8(%ebp)
movl $68280, -12(%ebp) # imm = 0x10AB8
cvtsi2sdl -12(%ebp), %xmm1
mulsd %xmm0, %xmm1
cvttsd2si %xmm1, %eax
movl %eax, -16(%ebp)
movl -16(%ebp), %eax
movl %edx, (%esp)
movl %eax, 4(%esp)
calll printf
xorl %ecx, %ecx
movl %eax, -20(%ebp) # 4-byte Spill
movl %ecx, %eax
addl $40, %esp
popl %ebp
這裡,你可以注意到它們有一條指令是cvttsd2si, 這是SSE2引入的指令,可以參考這裡描述http://x86.renejeschke.de/html/file_module_x86_id_66.html
Converts a double-precision floating-point value in the source operand (second operand) to a signed doubleword integer in the destination operand (first operand). The source operand can be an XMM register or a 64-bit memory location. The destination operand is a general-purpose register. When the source operand is an XMM register, the double-precision floating-point value is contained in the low quadword of the register. When a conversion is inexact, a truncated (round toward zero) result is returned. If a converted result is larger than the maximum signed doubleword integer, the floating-point invalid exception is raised, and if this exception is masked, the indefinite integer value (80000000H) is returned.
同時可以看這裡的注釋:CS 301 Lecture
Convert to ("2", get it?) Single Integer (si, stored in register like eax) or four DWORDs (dq, stored in xmm register). "cvtt" versions do truncation (round down); "cvt" versions round to nearest.
剩下的就不用解釋了。
那麼,為什麼GCC 32位要這樣做呢?原因就是為了兼容老的架構。而Clang則並未這麼做。Mac下面的Intel晶元則是一開始就有SSE。
那麼,如果在Linux下面,使用gcc -m32 a.c -msse2, 你也會得到20484了,而非20483了。
推薦文章:Tag - undefined behavior這篇文章也談到了因為SSE2指令的問題而造成clang / gcc 在float 與 int 轉換的不同行為。浮點數 0.3 實際上比實數 0.3 少一點點,所以和整數相乘的時候有機會比正確答案少一點。
轉換成整數時是用 truncation,只保留整數部分。
而printf會按設定的精度格式來 rounding。使用 "%.17g" 應可列印精確的值。你可以選擇用 round()、floor()、ceil()等方式轉為整數的浮點數,再轉換為 int。
----更新:我這裡只是說明為什麼可能會出現問題中的情況。不同 CPU、編譯器(及設置)都有可能影響每個浮點數運算的結果。@藍色 的回答並沒有解釋後兩個為什麼能正確輸出20484,所以在此補充一下。
其實我把源程序這樣改寫一下,就明白了(方便起見,使用C++語法聲明變數)。
為了突出關鍵部分,沒有使用代碼片段,方便加粗和斜體,請不要吐槽排版int main(int argc, char* argv[])
{
int a = 68280; int result0 = static_cast&", result0); double result1 = a * 0.3; printf("%f
", result1); float result2 = static_cast&
", result2); int result3 = static_cast&
", result3);
int result4 = static_cast&
", result4);}
也就是說:
result0: 在內存中的 a 乘以 常數double型的0.3result3: 常數68280 乘以 常數 0.3,得以優化為 常數 20484。(常數優化對於不能精確表示的浮點gcc是如何處理的我不知道,不過至少這個case,似乎結果是正確的。result4: 在內存中的 a 乘以 常數float型的0.3所以 result0 出了問題,原因其他各位都解釋的很清楚了result3 沒出,是因為原句等價於直接賦值 20484result4 沒出,是因為雖然仍有rounding問題,但是由於0.3不能被浮點數準確表示,單精度float的確切值和雙精度double的確切值並不完全相等,然後在這個special case下,兩者rounding結果正好不同----------------------分割線--------------------
順帶一說,如果你用老版本的VC++,例如vc7/vc8/vc9,默認x87的fp(而不是現在默認sse2)的話,目測不開啟優化結果和這裡gcc一樣。開啟優化後,理論上所有編譯器都會把上面的print全給你轉成print常數(gcc在不開啟優化的情況下轉了68280 * 0.3,msvc不開優化這個應該是不會轉的)VS 2015得到的結果,貌似是編譯器的問題....
斗膽說下,關於浮點數精度的問題,不止出現在c++。所有編程語言中都有同樣的問題。想下,0.3如何用二進位精確表示,又如何能在八位的限制下精確表示?
推薦閱讀: