浮點數之小數點漂移記
什麼是「浮點數」?
首先,你可曾想過這個問題,以前我們在數學課都叫它「小數」,為什麼到了程式這邊卻叫做「浮點數(Floating Point Number)」?是在浮什麼?把它丟在水裡會浮起來嗎?
簡單來說,電腦無法直接處理我們日常看到的十進位小數點(像是 3.14 或 0.001),它需要用一種比較特別的方式來表示這些數字,浮點數的結構類似於科學記號表示法,例如 3.14 可以寫成 3.14 x 100,0.001 可寫成 1 x 10-3。在電腦裡是用「有效數字(Mantissa)」和「指數(Exponent)」來表示。
想像一下,如果我們用固定小數點來表示 0.0000123 或是 123,000,000,000 之類的數字,當數字越大或越小,就會需要更多的 0,這可能會有點浪費空間。如果使用類似科學記號表示法的寫法就可以變成 1.23 x 10-5 和 1.23 x 1011,不需要一堆 0。
所以,浮點數的這個「浮」,是指小數點的位置可以自由變動,不用固定在哪一個位置上。這 樣的設計讓電腦可以有效率地表示非常大或非常小的小數,不會因為數字太大或太小而造成溢位(Overflow)的問題,但缺點就是可能會有一些精準度上的問題。
浮點數的結構
在 Python 裡的浮點數也是個物件,它的結構如下:
typedef struct {
PyObject_HEAD
double ob_fval;
} PyFloatObject;
跟上個章節講到的整數 PyLongObject
比起來,這個 PyFloatObject
的結構更簡單一點,它只有一個 double
型態的成員變數 ob_fval
來存浮點數的值。有些程式語言,例如 C 語言本身就是,有分 32 位元的單精度浮點數(float
)以及 64 位元的雙精度浮點數(double
),但 CPython 的原始碼來看,它的浮點數就直接用借 C 語言的 double
來實作浮點數。
當執行 a = 3.14
時,CPython 會建立一個新的 PyFloatObject
物件,這會由 Objects/floatobject.c
裡所定義的 PyFloat_FromDouble()
函數完成,我把一些目前對我們來說比較不重要的條件編譯以及 { }
區塊拿掉,看起來會像這樣:
PyObject *
PyFloat_FromDouble(double fval)
{
PyFloatObject *op;
op = PyObject_Malloc(sizeof(PyFloatObject));
if (!op) {
return PyErr_NoMemory();
}
_PyObject_Init((PyObject*)op, &PyFloat_Type);
op->ob_fval = fval;
return (PyObject *) op;
}
過程滿單純的,看起來是先呼叫 PyObject_Malloc()
函數要一塊記憶體,再 _PyObject_Init()
初始化,最後把傳入的 fval
指定給成員變數 ob_fval
。
嗯...其實也沒這麼單純,這是因為我先省略了一些程式碼所以看起來比較簡單,待會我們看到浮點數的效能的部分再來補充。
正因為 CPython 的浮點數是用 C 語言裡的 double
實作的,所以有些 Python 的浮點數的設計以及運算結果都會跟 C 語言一樣。
關於浮點數
CPython 的浮點數等於 C 語言的 double
,所以 CPython 的浮點數也跟 C 語言一樣都是按照 IEEE 754 標準來實作的。IEEE 754 對於雙精度浮點數是使用 64 個位元來表示一個數:
63 62 52 0
+---+----------+---------------------------------------+
| S | E | M |
+---+----------+---------------------------------------+
這裡我用幾個簡單的字母來表示:
S
是符號位元(Sign),佔 1 位元,0
代表正數,1
代表負數。E
是指數位元(Exponent),佔 11 位元。M
是尾數位元(Mantissa),佔 52 位元。
舉例來說,3.14 的二進位表示法算起來會是:
11.0010001111010111000010100011110101...
雖然這是一個除不盡的無限循環小數,但我們還是得用有限的資源或方式來表示它,所以會有不精準是很正常的。改用類似科學記號表示法的方式來表示,小數點往左移一位,變成:
1.10010001111010111000010100011110101... x 2^1
最後再補充一下 E
也就是指數位元的計算方式,這稍微複雜一點點。IEEE 754 是採用「指數偏移值」的方式計算,以 double
來說它的偏移值(Bias)是 210 - 1,也就是 1,023。現在我們知道 3.14 換成二進位會表示成 1.100100011.. x 2^1
,這裡 21 的 1
就是指數位元 E
,再加上偏移值 1,023 變成 1,024,換成二進位為 10000000000
。使用偏移值來表示指數的原因是為了讓指數能夠用更簡單的方式來表達正的指數以及負的指數。
接著用 IEEE 754 的雙精度浮點數來表示的話,會是:
S
是0
,因為 3.14 是正數。E
剛才算出來了,是10000000000
。M
是小數點左邊的那一串10010001111010111000010100011110101...
所以 3.14 轉換成 IEEE 754 的表示法會是:
0 10000000000 1001000111101011100001010001111010111000010100011110
從這裡也可以看的出來 M
部份根本沒辦法裝下所有的位數,所以大家常會說浮點數不精準,原因就是這樣。C 語言的 double
是照著 IEEE 754 標準來實作的,從原始碼就能看的出來 CPython 的浮點數其實就是 C 語言的 double
,所以也有不精準的問題。
浮點數的運算
回來看一下 PyFloat_Type
結構的定義:
PyTypeObject PyFloat_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"float",
// ... 略 ...
&float_as_number, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_clear */
0, /* tp_as_mapping */
// ... 略 ...
};
之前有介紹過 tp_as_
這幾個成員變數的用途,如果要用來做四則運算的時候,看的就是 tp_as_number
成員的設定,順著 float_as_number
追過看看:
static PyNumberMethods float_as_number = {
float_add, /* nb_add */
float_sub, /* nb_subtract */
float_mul, /* nb_multiply */
float_rem, /* nb_remainder */
float_divmod, /* nb_divmod */
float_pow, /* nb_power */
(unaryfunc)float_neg, /* nb_negative */
float_float, /* nb_positive */
(unaryfunc)float_abs, /* nb_absolute */
(inquiry)float_bool, /* nb_bool */
0, /* nb_invert */
// ... 略 ...
};
光從名字就能猜到這些方法對應到就是四則運算之類的函數了,我們來追個加法 float_add
跟減法 float_add
看看:
static PyObject *
float_add(PyObject *v, PyObject *w)
{
double a,b;
CONVERT_TO_DOUBLE(v, a);
CONVERT_TO_DOUBLE(w, b);
a = a + b;
return PyFloat_FromDouble(a);
}
static PyObject *
float_sub(PyObject *v, PyObject *w)
{
double a,b;
CONVERT_TO_DOUBLE(v, a);
CONVERT_TO_DOUBLE(w, b);
a = a - b;
return PyFloat_FromDouble(a);
}