讀源碼學MYSQL系列(二)decimal存儲轉化函數decimal2bin

問題來源

  高精度計算是計算機工程實踐中非常重要的內容,在涉及到精確計算的項目中,思考過數據庫的設計。因而比較好奇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類型的數據的。

參考

github decimal源代碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章