問題來源
高精度計算是計算機工程實踐中非常重要的內容,在涉及到精確計算的項目中,思考過數據庫的設計。因而比較好奇MYSQL中是如何實現對decimal的支持的。本文通過源碼閱讀,分析理解decimal的存儲及各種運算轉化。參考源代碼:
https://github.com/google/mysql/blob/master/include/decimal.h
https://github.com/twitter-forks/mysql/blob/master/strings/decimal.c
基礎準備
首先,在頭文件decimal.h中定義了基本結構體和類型:
typedef int32 decimal_digit_t;
MYSQL採取4字節爲一組來存儲高精度小數,可以存儲9位十進制數字。不足9位部分仍然使用4字節存儲。
typedef struct st_decimal_t {
int intg, frac, len;
my_bool sign;
decimal_digit_t *buf;
} decimal_t;
其中,各個字段含義如下:
intg: 整數,十進制整數部分位數
frac: 整數,十進制小數部分位數
sign: 布爾,false表示正數,true表示負數
buf: int32類型數組,每個int32存儲9位十進制數字
len: 數組buf的長度
還有一些其他的宏定義如下:
typedef decimal_digit_t dec1;
typedef longlong dec2;
#define DIG_PER_DEC1 9
#define DIG_MASK 100000000
#define DIG_BASE 1000000000
#define DIG_MAX (DIG_BASE-1)
#define DIG_BASE2 ((dec2)DIG_BASE * (dec2)DIG_BASE)
#define ROUND_UP(X) (((X)+DIG_PER_DEC1-1)/DIG_PER_DEC1)
static const dec1 powers10[DIG_PER_DEC1+1]={
1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000};
static const int dig2bytes[DIG_PER_DEC1+1]={0, 1, 1, 2, 2, 3, 3, 4, 4, 4};
static const dec1 frac_max[DIG_PER_DEC1-1]={
900000000, 990000000, 999000000,
999900000, 999990000, 999999000,
999999900, 999999990 };
存儲轉化函數decimal2bin
該函數將decimal_t類型的結構體轉化爲二進制形式。下面結合源代碼說明,爲了便於理解,將整個函數按照功能順序拆分成幾個模塊分析。
思路說明
前文說過,decimal是採用4字節來存儲9位整數的,該函數主要圍繞着該思路進行。對於任意一個小數,可以進行如下拆分:
不足9位部分|9位整數(重複多次)|小數點|9位小數(重複多次)|不足9位部分
函數聲明
int decimal2bin(decimal_t *from, uchar *to, int precision, int frac)
各個參數含義如下:
from: 待轉化的decimal結構體
to: uchar數組,轉化結果保存到該數組
precision: 聲明decimal精度中的總位數
frac: 聲明decimal精度中的小數位數
該函數返回執行結果的狀態碼:
E_DEC_OK/E_DEC_TRUNCATED/E_DEC_OVERFLOW
變量聲明
dec1 mask=from->sign ? -1 : 0, *buf1=from->buf, *stop1;
int error=E_DEC_OK, intg=precision-frac,
isize1, intg1, intg1x, from_intg,
intg0=intg/DIG_PER_DEC1,
frac0=frac/DIG_PER_DEC1,
intg0x=intg-intg0*DIG_PER_DEC1,
frac0x=frac-frac0*DIG_PER_DEC1,
frac1=from->frac/DIG_PER_DEC1,
frac1x=from->frac-frac1*DIG_PER_DEC1,
isize0=intg0*sizeof(dec1)+dig2bytes[intg0x],
fsize0=frac0*sizeof(dec1)+dig2bytes[frac0x],
fsize1=frac1*sizeof(dec1)+dig2bytes[frac1x];
const int orig_isize0= isize0;
const int orig_fsize0= fsize0;
uchar *orig_to= to;
該函數聲明的變量比較多,有一部分是在計算過程中使用到的,可以到時候再根據代碼判斷含義。在此主要說明初始化過的變量:
mask: 符號位,dec1類型,其實就是int類型
buf1: 指向from數據中的buf數組
intg: 計算出來的整數部分位數
intg0, frac0, intg0x, frac0x是根據類型精度參數decimal(precision, frac)計算出來的各部分長度。按照前文的拆分,可以表示如下:
intg0x | intg0 | 小數點 | frac0 | frac0x
相應的,isize0和fsize0分別表示計算出來的整數部分和小數部分的字節數。
frac1和frac1x表示實際傳進來的from參數的小數部分:
整數部分 | 小數點 | frac1 | frac1x
fsize1表示小數部分需要的字節數。
那麼此處爲什麼不計算整數部分呢?整數部分涉及到前導0的移除,相對要複雜一點,其計算放到了下面的代碼中。
前置0處理
buf1= remove_leading_zeroes(from, &from_intg);
if (unlikely(from_intg+fsize1==0))
{
mask=0; /* just in case */
intg=1;
buf1=&mask;
}
intg1=from_intg/DIG_PER_DEC1;
intg1x=from_intg-intg1*DIG_PER_DEC1;
isize1=intg1*sizeof(dec1)+dig2bytes[intg1x];
remove_leading_zeros函數的功能通過名字就可以看出,移除前置0,該函數功能比較簡單,只有十幾行,可以自行查看。from_intg返回移除之後的整數部分位數,返回的buf1指向數組中從左到右第一個非0的元素位置。
爲了理解if語句,需要解釋一下unlikely。在mysql源碼中,likely和unlikely都出現過,從用法上來說是一樣的,if (likely(value))等價於if(value),unlikely類似。那有什麼用處呢?其實它們是編譯優化語句,likely表示if語句塊有較大概率執行,unlikely表示else語句塊有較大概率執行,方便編譯器做指令優化用的。
條件from_intg+fsize1==0表示整數部分是0,同時小數部分也爲0,此時做一些例外處理。因爲這是一個較小概率的情形,所以使用了unlikely。
回答上一節的疑問,在這裏,intg1和intg1x分別表示移除前置0後的整數部分中足9位和不足9位部分,isize1則表示相應的字節數。如下:
intg1x | intg1 | 小數點 | 小數部分
空間檢查
整數部分
if (intg < from_intg)
{
buf1+=intg1-intg0+(intg1x>0)-(intg0x>0);
intg1=intg0; intg1x=intg0x;
error=E_DEC_OVERFLOW;
}
else if (isize0 > isize1)
{
while (isize0-- > isize1)
*to++= (char)mask;
}
先看if條件,intg是根據聲明規格計算出來的整數部分位數,如果小於實際from_intg,報溢出錯誤。buf1指向實際存儲數位的數組,intg1-intg0是足9位部分超出的個數。(intg1x>0)指實際數據中不足9位部分是否需要一個int元素,類似的,(intg0x>0)指聲明規格中不足9位部分是否需要一個int元素。buf1加上這些超出部分,實際含義是跳過from中比聲明規格多的部分。intg1和intg1x也要修正爲聲明規格。
在else分支中,如果根據規格計算出來的字節數isize0大於實際字節數isize1,移動to指針,同時將多出的字節設置成符號位mask。
小數部分
if (fsize0 < fsize1)
{
frac1=frac0; frac1x=frac0x;
error=E_DEC_TRUNCATED;
}
else if (fsize0 > fsize1 && frac1x)
{
if (frac0 == frac1)
{
frac1x=frac0x;
fsize0= fsize1;
}
else
{
frac1++;
frac1x=0;
}
}
首先,看外層if條件,如果規格計算值fsize0小於實際字節數fsize1,報溢出錯誤,並將frac1和frac1x修正爲聲明規格參數。注意,對比整數部分,此處不需要移動buf1指針。因爲,超出的小數部分位於數組的末尾。
然後,在else分支中,如果規格計算值fsize0大於實際字節數且實際frac1x大於0(即小數部分有不足9位的剩餘部分出現),爲了方便後續計算,需要對位數進行一些調整,繼續分兩種情況。一是frac0==frac1,此時必有frac0x>frac1x,需要對frac1x進行修正。二是frac0>frac1,此時,將frac1x直接併入到frac1中一起處理。注意,因爲fsize0>fsize1且frac1x>0,不可能有frac0<frac1。
爲了便於後續小數部分的處理,在此做一個總結。經過上段代碼後,小數部分的可能關係如下:
當fsize0 < fsize1時, frac1 = frac0, frac1x = frac0x
當fsize0 = fsize1時, frac1 = frac0, frac1x = frac0x or frac1 - frac0 = 1, frac1x = 0, frac0x=7/8 or frac0 - frac1 = 1, frac0x = 0, frac1x = 7/8
當fsize0 > fsize1時, frac0 >= frac1, frac0x >= frac1x
數值轉換
根據拆分成9位部分和不足9位部分的思路,可以將要轉化的數劃分爲以下結構:
intg1x | intg1 | 小數點 | frac1 | frac1x
轉化過程分成這四部分展開:
intg1x
/* intg1x part */
if (intg1x)
{
int i=dig2bytes[intg1x];
dec1 x=(*buf1++ % powers10[intg1x]) ^ mask;
switch (i)
{
case 1: mi_int1store(to, x); break;
case 2: mi_int2store(to, x); break;
case 3: mi_int3store(to, x); break;
case 4: mi_int4store(to, x); break;
default: DBUG_ASSERT(0);
}
to+=i;
}
dig2bytes函數根據數字位數(intg1x)計算需要的字節數。此處,*buf1即是最高位的那個int整數,求餘操作保證不會超出intg1x位數限制,最後與符號位mask進行了異或操作。
得到了要保存的數值後,依據字節數i,分別將x存儲到to所指向的數組中。mi_int1store表示將x的一個字節存儲到to地址中,其他類似。
intg1 + frac1
/* intg1+frac1 part */
for (stop1=buf1+intg1+frac1; buf1 < stop1; to+=sizeof(dec1))
{
dec1 x=*buf1++ ^ mask;
DBUG_ASSERT(sizeof(dec1) == 4);
mi_int4store(to, x);
}
接下來是整數和小數中的足9位部分,這個比較簡單,一個一個複製int就可以了。參考上面的代碼,每次複製一個int到to數組中,直到遍歷完intg1和frac1次。值得注意的是,在保存數值的時候,同樣的,與符號位mask進行了異或操作。
frac1x
/* frac1x part */
if (frac1x)
{
dec1 x;
int i=dig2bytes[frac1x],
lim=(frac1 < frac0 ? DIG_PER_DEC1 : frac0x);
while (frac1x < lim && dig2bytes[frac1x] == i)
frac1x++;
x=(*buf1 / powers10[DIG_PER_DEC1 - frac1x]) ^ mask;
switch (i)
{
case 1: mi_int1store(to, x); break;
case 2: mi_int2store(to, x); break;
case 3: mi_int3store(to, x); break;
case 4: mi_int4store(to, x); break;
default: DBUG_ASSERT(0);
}
to+=i;
}
對比intg1x部分的處理,前面多了一個while循環。這裏的邏輯比較繞,while的作用是爲了擴充frac1x,舉個例子,0.345,frac1 = 0, frac1x = 3,此時,通過while循環可以把frac1x擴充爲4,即0.3450,分別以34和50的形式存儲到uchar數組中。那麼,此處的lim是怎麼計算的呢?參考上一行代碼,這裏有一個大前提,不能超過聲明規格,即isize0 >= isize1。當frac1 < frac0時,frac1x的取值從0到DIG_PER_DEC1都有可能,所以lim = DIG_PER_DEC1。而當frac1 >= frac0時,必有frac1x <= frac0x,所以lim = frac0x。
收尾
if (fsize0 > fsize1)
{
uchar *to_end= orig_to + orig_fsize0 + orig_isize0;
while (fsize0-- > fsize1 && to < to_end)
*to++= (uchar)mask;
}
orig_to[0]^= 0x80;
/* Check that we have written the whole decimal and nothing more */
DBUG_ASSERT(to == orig_to + orig_fsize0 + orig_isize0);
return error;
if條件表示在to數組大於實際需要的情況下,使用符號位mask對to數組進行填充。
最後返回該函數執行的狀態碼。
總結
該函數雖然只有120多行,但有較多的細節,因爲涉及到from數據和to數組之間的規格檢測及空間填充問題。通過該函數,我們可以比較清楚的看到mysql是如何存儲decimal類型的數據的。