Redis源碼中看偽隨機數產生演算法

來源:互聯網
上載者:User

標籤:drand48   lrand48   偽隨機數產生演算法   隨機數種子   redis源碼   

導讀--------------------------------------------------------------------------------------------------------------------------------------------------------------

        Redis源碼中有一個rand.c的源檔案,很明顯這是一個和(偽)隨機數有關的檔案。細看該檔案代碼只有寥寥50行,不過涉及到的演算法原理卻不簡單,讀起來雖然有些晦澀,但對於深入理解48位空間中的偽隨機數演算法是不可多得的範本。作者在該檔案的注釋中寫道:這個偽隨機數產生函數是從pysam源碼中的drand48()派生過來的。關於pysam是什麼項目,並不是重點,其實很多Unix系統中都存在drand48這個函數(SVr4,POSIX.1-2001),我們可在終端中man一下drand48。

        可以看到在標頭檔stdlib.h中定義了一系列和隨機數有關的函數,除drand48()外,還有erand48()、lrand48()、nrand48()等等。所謂的48指的是隨機數種子的二進位位元。它們的主要操作使用的都是同一個隨機數產生演算法,但是後續的處理不同,比如drand48()、erand48()返回的是 [0.0, 1.0)之間的浮點型,而lrand48()和nrand48()返回的是[0, 2^31)之間的整數。rand.c檔案中有一個重要的函數——redisLrand48()。雖然作者是受了drand48()源碼的啟發編寫的該函數,但實際上redisLrand48()和lrand48()更像,擁有幾乎一樣的編程介面,並且設定相同的隨機數種子,產生的偽隨機數序列相同。

        繼續讀rand.c開頭的注釋,可以發現作者編寫這個檔案的目的是用於替換Lua中實現的math.random()函數。作者認為Lua隨機數產生演算法的預設實現中使用了libc的rand()函數,而libc的rand()並不能保證在設定了相同種子的情況下,不同平台能產生相同的隨機數序列。因此作者重寫這一檔案,保證跨平台性。

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

演算法原理線性同餘方程        該偽隨機數產生演算法使用了線性同餘方程,顧名思義:所謂 “線性”,指的是自變數(方程中的x)和因變數(方程中的y)是線性關係;所謂 “同餘”,表示它們與同一個數(比如m)相除,餘數相同。可以表示為:y = f(x)%m 或 y = f(x) mod m 【f(x)是線性函數,即x的次數為1】        在Linux中,通過lrand48的手冊頁,可以看到lrand48()所使用的線性同餘方程的具體內容(Redis使用的是同一個方程):
Xn+1 = (aXn + c) mod m, where n >= 0//預設情況下a = 0x5DEECE66Dc = 0xBm = 2^48 
X0、X1、X2……Xn就是偽隨機數序列。它們每一個都要由上一個數字通過運算來產生。
種子        隨機數產生演算法中都有種子的概念,無論是普通的rand()還是lrand48()。我們前面多次提到 偽隨機數這一術語,之所以說“偽”,是因為當種子確定之後,此後產生到隨機數序列也就確定了。換句話說只要是相同的種子,產生的隨機數序列總是固定的(相同的平台下),並非完全隨機。        通過上一小節中的線性同餘方程,我們觀察到隨機數序列的產生受到X0、a、c的影響(m總是2^48,表示48位空間中的運算,所以這些函數尾碼才會有48)。因而X0、a、c其本質就是種子。POSIX的lrand48()的實現中和Redis的實現中給這三個參數提供了相同預設值。但不同之處是POSIX標準提供了lcong48()函數來修改這三個參數的預設值,而Redis中僅僅能修改X0(表示隨機數序列的第一個數)的值,所以在Redis中真正的只有一個——X0.源碼實現變數        rand.c中定義了三個靜態全域變數:x、a、c 與線性同餘方程中的參數相對應
static uint32_t x[3] = { X0, X1, X2 }, a[3] = { A0, A1, A2 }, c = C;
這裡涉及到幾個宏定義:
#define X00x330E#define X10xABCD#define X20x1234#define A00xE66D#define A10xDEEC#define A20x5#define C0xB
        可以看出數組a中的三個元素儲存的就是線性同餘方程中的常數 0x5DEECE66D的三個部分。注意的是a中每個元素是uint32_t類型,就是說4個位元組,實際上我們把0x5DEECE66D拆成三個部分(0x5、0xDEEC、0xE66D),每個部分最多隻佔用了2個位元組。另外要注意的是宏定義中的X0和前文(上一節)中的X0語義是不同的,這裡的X0表示的是數組X預設初始化時的第一個元素的內容,而上一節中提到的X0表示的是第一個隨機數,相當於本節中的X2*2^32 + X1*2^16 + X0。
對外函數        rand.c對外只提供了兩個函數:
  • redisLrand48():返回一個隨機數
  • redisSrand48():設定隨機數種子
另外還有一個靜態函數next(),它是為redisLrand48()服務的,對外不可見。redisSrand48()函數
void redisSrand48(int32_t seedval) {    SEED(X0, LOW(seedval), HIGH(seedval));}
        設定隨機數的種子,用到了宏函數SEED。看一下定義:
#define SET3(x, x0, x1, x2)((x)[0] = (x0), (x)[1] = (x1), (x)[2] = (x2))#define SEED(x0, x1, x2) (SET3(x, x0, x1, x2), SET3(a, A0, A1, A2), c = C)
        可以看成宏函數SEED進行了三個賦值操作,分別是給數組x、數組a和變數c。不過a和c賦的都是預設值,所以SEED(X0,LOW(seedval),HIGH(seedval))實際進行的操作就是用X0(0x330E)、seedval的低16位,seedval的高16位分別賦值給x[0]、x[1]、x[2]。LOW和HIGH也是兩個宏函數
#define N16 #define MASK((1 << (N - 1)) + (1 << (N - 1)) - 1) //MASK=65535(16個二進位位1)#define LOW(x)((unsigned)(x) & MASK)                //LOW(x)獲得3x的低16位(2個位元組)#define HIGH(x)LOW((x) >> N)                         //HIGH(x)獲得x的高16位(2個位元組)

注意上面所指的高16位和低16位指的是32位整型資料的高低。最終每次要保留的結果是48位的(儲存在數組x[ ]中),它分為高16位(x[2])、中16位(x[1])和低16位(x[0])。不要混淆。

redisLrand48()函數
int32_t redisLrand48() {    next();    return (((int32_t)x[2] << (N - 1)) + (x[1] >> 1));}
        next函數容後再稟,先看一下返回值,把N=16帶進去,就是:
((int32_t)x[2] << 15) + (x[1] >> 1)
就是x[2]*2^15 + x[1]/2。這裡沒有什麼道理可講,隨機數嘛,這隻是一種方案而已(lrand48()採用的方案)。next()函數公式推導與化簡        next()是為redisLrand48()服務的static函數,也就是說對外不可見的,但是next()函數才是偽隨機數產生演算法的精髓所在。再回顧一遍線性同餘公式:
Xn+1 = (aXn + c) mod m, where n >= 0,m = 2^48
        注意:n和n+1都是X的下標。從數學角度來看很簡單啊,先乘再加,最後模數。但是電腦做起來卻不盡然,因為會溢出,所以我們是用數組儲存的x和a。所以要做的是用數組類比兩個數的乘法運算。先把方程中的X和a表示出來:


對線性同餘方程進行一下換算:
Xn+1 = (aXn + c) mod mXn+1 = ((aXn) mod m + c mod m) mod mXn+1 = ((aXn) mod m + c) mod m
        先計算一下a*Xn:
        這個多項式共有9個部分組成。再計算一下(a*Xn) mod m。因為m = 2^48,所以上述多項式中,2的冪次大於等於48的項一定是2^48的倍數,模數之後就是0,故可去掉這些項。所以(a*Xn)mod m的結果為:
        只剩下6項了。接著合并同類項,並加上c,很容易寫出 a*Xn + c的結果:
        上述算式的每個括弧中的內容就是在一次隨機數產生之後x[2]、x[1]、x[0] 裡面儲存的新的內容(即用來表示Xn+1)。當然了編程去計算的時候還要考慮進位的問題。先看一個宏定義:用到的宏
#define MUL(x, y, z){ int32_t l = (long)(x) * (long)(y); (z)[0] = LOW(l); (z)[1] = HIGH(l); }#define CARRY(x, y)((int32_t)(x) + (long)(y) > MASK)#define ADDEQU(x, y, z)(z = CARRY(x, (y)), x = LOW(x + (y)))
        很明顯宏函數MUL(x,y,z)表示的是乘法運算,x和y相乘,結果用z來儲存。z[0]儲存低16位,z[1]儲存高16位。        CARRY(x,y)就是判斷兩個數相加是否會進位”,這裡的“進位”指的超出2個位元組(16位)的大小。因為我們無論是數組a還是數組x,其中每個元素儲存的都是2個位元組的有效資料。也就是說,如果x[0]裡面的數字超過2個位元組,就要把多出的部分“進位”到x[1]中。每個元素其實都是4個位元組的大小,之所以沒有用到全部的4位元組來儲存,是因為當4位元組全部用來儲存資料時,相加以後可能就會溢出,編譯器會彈出警告,其結果也會變成負數。
        ADDEQU(x,y,z)執行的操作是:x和y相加,其結果儲存到x中。z中儲存是否“進位”。next()源碼
static void next(void) {    uint32_t p[2], q[2], r[2], carry0, carry1;    MUL(a[0], x[0], p);    ADDEQU(p[0], c, carry0);    ADDEQU(p[1], carry0, carry1);    MUL(a[0], x[1], q);    ADDEQU(p[1], q[0], carry0);    MUL(a[1], x[0], r);    x[2] = LOW(carry0 + carry1 + CARRY(p[1], r[0]) + q[1] + r[1] +            a[0] * x[2] + a[1] * x[1] + a[2] * x[0]);    x[1] = LOW(p[1] + r[0]);    x[0] = LOW(p[0]);}
        我們來一行一行的分析一下。聲明語句不看,從第4行開始讀。
    MUL(a[0], x[0], p);    ADDEQU(p[0], c, carry0);    ADDEQU(p[1], carry0, carry1);

  • MUL運算:a[0]和x[0]相乘,結果儲存到數組p中。
  • 第一個ADDEQU:然後p[0]再和c相加,carry0標識是否有“進位”。這裡完成的就是運算。
  • 第二個ADDEQU之後就是把低16位加上c之後是否進位,加到p[1],並且判斷這次相加之後有沒有產生新的進位(指2^16次多項式向2^32次多項式的進位)並儲存到carry1。

至此一次多項式相關的運算計數完畢,接下來是2^16次多項式的運算。


    MUL(a[0], x[1], q);    ADDEQU(p[1], q[0], carry0);    MUL(a[1], x[0], r);
  • 第一個MUL運算:a[0]和x[1]相乘,結果儲存到數組q中。
  • ADDEQU運算:將p[1](儲存了“低16位”運算之後向“中16位”進位的值)和q[0]相加,結果儲存到p[1],carry0標識“中16位”(2^16次多項式)對高16位(2^32次多項式)是否有新的進位。
  • 第二個MUL運算:a[1]和x[0]相乘,結果儲存到數組r中。
至此完成了係數相關運算。
    x[2] = LOW(carry0 + carry1 + CARRY(p[1], r[0]) + q[1] + r[1] +            a[0] * x[2] + a[1] * x[1] + a[2] * x[0]);    x[1] = LOW(p[1] + r[0]);    x[0] = LOW(p[0]);
        倒著讀吧,先看x[0],它儲存的應該是一次多項式的結果。        x[1]儲存2^16次多項式的係數。p[1]裡面儲存著a[0]x[1]的結果以及低16位的進位。r[0]儲存著a[1]x[0]的結果(不包括進位)。
        x[2]最複雜。回顧一下2^32次多項式的係數:再看一眼,那行代碼,這幾個兩兩直接相乘的部分讀懂了吧,那麼可以去掉他們再看其他部分:carry0 + carry1 +CARRY(p[1], r[0]) + q[1] + r[1]剩餘的多項式就是2^16次多項式在運算過程中向上的進位了。
        畫了一個圖給大家表示一下: 
        這個圖的上面兩行是表頭,下面彩色部分是所儲存的內容。比如第三列表示x[0]裡面儲存的是p[0]和c的和。        從第一幅圖到第二幅圖的轉變是因為執行了 ADDEQU(p[1], q[0], carry0);這條語句,將q[0]加到了p[1]上。然後再去看看x[2]的那段代碼,就清楚多了吧,重點是考慮進位。

Redis源碼中看偽隨機數產生演算法

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.