浮點數的比較

在數學運算當中經常會涉及到判斷兩個數是否相等的情況
對於整數很好處理 A==B這樣的一個語句就可以解決全部的問題
但是對於浮點數是不同的

首先,浮點數在計算機當中的二進制表達方式就決定了大多數浮點數都是無法精確的表達的
現在的計算機大部分都是數字計算機,不是模擬機,數字機的離散化的數據表示方法自然無法精確表達大部分的數據量的。

其次計算機浮點數的精度在單精度float類型下,只有7位,在進行浮點運算的時候,這個精度往往會導致運算的結果和實際期望的結果之間有誤差

因爲前兩個原因,我們很難用 A==B來判定兩個浮點數是否相同

很自然,我們可以想到 fabs(A-B) < epsilon 這樣的一種判別方法
但是這種判別方法穩妥嗎?
它也不穩妥。

首先, epsilon是一個絕對的數據,也就是誤差分析當中說說的絕對誤差
使用一個固定的數值,對於float類型可以表達的整個數域來說是不可以的
比如epsilon取值爲0.0001,而a和b的數值大小也是0.0001附近的,那麼顯然不合適
另外對於a和b大小是10000這樣的數據的時候,它也不合適,因爲10000和10001也可以認爲是相等的呢
適合它的情況只是a或者b在1或者0附近的時候

既然絕對誤差不可以,那麼自然的我們就會想到了相對誤差
bool IsEqual(float a, float b, float relError ) {
       return ( fabs ( (a-b)/a ) < relError ) ? true : false;
}
這樣寫還不完善,因爲是拿固定的第一個參數做比較的,那麼在調用
IsEqual(a, b, relError ) 和 IsEqual(b, a, relError ) 的時候,可能得到不同的結果
同時如果第一個參數是0的話,就有可能是除0溢出
這個可以改造
把除數選取爲a和b當中絕對數值較大的即可
bool IsEqual(float a, float b, relError )
{
      if (fabs(a)<fabs(b)) return ( fabs((a-b)/a) > relError ) ? true : false;
      return (fabs( (a-b)/b) > relError ) ? true : false;
};

使用相對誤差就很完善嗎?
也不是, 在某些特殊情況下, 相對誤差也不能代表全部
比如在判斷空間三點是否共線的時候,使用判斷點到另外兩個點形成的線段的距離的方法的時候
只用相對誤差是不夠的,應爲線段距離可能很段,也可能很長,點到線段的距離,以及線段的長度做綜合比較的時候,需要相對誤差和絕對誤差結合的方式纔可以
相對完整的比較算法應該如下:
bool IsEqual(float a, float b, float absError, float relError )
{
         if (a==b) return true;
        if (fabs(a-b)<absError ) return true;
        if (fabs(a>b) return (fabs((a-b)/a>relError ) ? true : false;
        return (fabs((a-b)/b>relError ) ? true : false;
}
這樣才相對完整

這僅僅是浮點數之間最初級的比較方法
高級的方法看 浮點數比較(二) 這個文章 —— 如何把兩個浮點數之間的比較轉化成爲兩個整數之間的比較。
-------------------------------------

浮點數的內存結構


根據IEEE的標準,浮點數的定義如下

符號位 指數位 小數部分 指數偏移量
單精度浮點數 1 位[31] 8位 [30-23] 23位 [22-00] 127
雙精度浮點數 1 位[63] 11 位[62-52] 52 位[51-00] 1023

我們以單精度浮點數來說明:
符號位,表述浮點數的正或者負
指數實際也有正負的,但是沒有單獨的符號位,而是採用了一個偏移來表示
在計算機的世界裏,進位都是二進制的,指數表示的也是2的N次冪
這個數據格式當中的,指數是8位,可表達的範圍是0到255
而對應的實際的指數是-127到+128
這裏特殊說明,-127和+128這兩個數據在IEEE當中是保留的用作多種用途的
-127表示的數字是0
128和其他位數組合表示多種意義,最典型的就是NAN狀態

小數部分,並不是一個浮點數的實際的小數
實際的小數在這個小數前面還保留了一個1
拿浮點數1.0來說
符號位是0, 實際指數是0,對應這裏的指數就是127了,也就是0x7f
而小數部分就是1.0了, 1是暗含的不存儲,實際的小數部分就是0了
因此組合起來的數據就是,0x3f80000

可以用一個類來表示:
class FloatType
{
public:
      union {
         DWORD m_dwInt;
         float          m_fFloat;
       struct {  

        int    m_nFra: 23;

          int    m_nExp : 8;

          bool m_bSign : 1;

      };
};

-----------------------------

參考IEEE的浮點數格式說明
對於0到1範圍內的浮點數是可以壓縮的

顯然在0到1的範圍內,一個單精度的浮點數,指數和符號位佔據9個bit
而這9個bit是可以不用的,把它去除,只保留小數部分的23bit就可以達到壓縮的目的
可以把一個浮點數從32bit,4字節壓縮到23bit,3字節的範圍內

這也是在3dmax等一些工具軟件當中對浮點數進行壓縮存儲的方法。
比如,在單位化的法向量當中,每個浮點數都是0,1範圍之間的數據
正常情況下表示三維空間當中的單位化法向量就需要12個字節
而經過這個壓縮處理,只需要9個字節

-----------------------------


在寫了上篇 浮點數的比較 以及 浮點數內存結構 兩篇文章後
對於浮點數的比較有新的想法

我們先看正數的情況
根據IEEE的內存結構, 指數在高位,尾數在低位
浮點數大的對應的把其內存結構按照整數來理解進行比較的時候,情況也是成立的
因此在這裏如果把他們進行比較的話,作爲整數運算效率會非常的高,比如
float f1 = 1.23;
float f2 = 1.24
f1 > f2 成立
(int&)f1 > (int&)f2 也是成立的

而且,仔細研究IEEE的浮點結構,可以發現在《浮點數比較》當中提到的浮點數精度的問題——不是所有的浮點數都可以精確的表達
可以精確表達的浮點數實際上是有限的,就是那些IEEE的各種情況的枚舉了 2^32個。不能表達的佔據了大多數

IEEE在32位的情況下,尾數是23位的(暗含了第一個位數是1)
對於可以精確表達的浮點數來說,如果我們把這23位當作整數來理解, 它加1,就意味着可以找到比當前對應浮點數大的最小的浮點數了
反之,我們把兩個浮點數,對應的整數做差值運算,得到的整數表明的是兩個浮點數之間有多少個實際可以表達的浮點數(對應的指數相同的情況下很好理解;指數不同的時候,也是同樣有效的)

這樣,對於兩個正的浮點數,他們的大小比較就可以用 (int&)f1 - (int&)f2 來進行比較了
差值的結果實際上就應該是相對誤差了
這個相對誤差,不等同於普遍意義上的相對誤差
它所表達的是,兩個浮點數之間可能還有多少個可以精確表達的浮點數
這樣通過指定這個閾值來控制兩個浮點數的比較就更有效了
對於兩個正的浮點數
bool IsEqual(float f1, float f2, int absDelta)
{
     if ( abs ( (int&)f1 - (int&)f2 ) < absDelta ) return true;
}
這裏用abs而不是fabs這在asm上面的運算差距也是很大的了

對於兩個負數進行比較的情況也是相同的
只不過負數內存對應的整數加1,相應的找到的是更小的負數而已

但是負數和整數之間現在還不能進行直接的比較,因爲根據IEEE的內存結構
正數和負數是不同的,對應的整數不能連續
正的最小的數就是0了,對應的整數也是0x00000000
負的最小的數就是-0,對應的整數則是0x 80000000
不用奇怪-0
在IEEE的表達當中是有兩個0的,一個是 +0 一個是-0

有趣的是,按照 f1 == f2 的判斷 +0和-0是相等的

通過對比我們可以發現,
+0 和正的浮點數可以按照轉換成爲整數的方式直接進行比較
-0 和負的浮點數可以按照轉換成爲整數的方式直接進行比較

如果我們能夠把他們連接起來,整個整數方式的直接比較就完備了
對比一下負數的結構, 可以找到一個簡單的辦法了:
        把負數內存對應的整數減去 -0 ,他們就連續了
而且更好的結果是,所有的負數經過這次減法後,對應的整數也都是負數了
這樣整個整數比較就變得連續了,而且在整個浮點數範圍內都是有效的了

最後的比較算法就是:
// 函數:   bool IsEqual(float f1, float f2, int absDelta)
// 功能:把比較兩個浮點數是否近似相同
// 輸入:f1, f2參與比較的兩個浮點數
//               absDelta 兩個浮點數之間允許有多少個其他可以精確表達的浮點數存在,相當於相對誤差
// 輸出:   true,兩個浮點數進行相等; false 兩個浮點數不等
// 注意:僅僅適合IEEE 32位浮點數結構
bool IsEqual(float f1, float f2, int absDelta)
{
       int i1, i2;
       i1 = ( f1>0) ? ((int&)f1) : ( (int&) f1 - 0x80000000 );
       i2 = (f2>0) ? ((int&)f2) : ( (int&) f2 - 0x80000000 );
       return   ((abs(i1-i2))<absDelta) ? true : false;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章