标签: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()并不能保证在设置了相同种子的情况下,不同平台能生成相同的随机数序列。因此作者重写这一文件,保证跨平台性。
--------------------------------------------------------------------------------------------------------------------------------------------------------------
Xn+1 = (aXn + c) mod m, where n >= 0 //默认情况下 a = 0x5DEECE66D c = 0xB m = 2^48
static uint32_t x[3] = { X0, X1, X2 }, a[3] = { A0, A1, A2 }, c = C;这里涉及到几个宏定义:
#define X0 0x330E #define X1 0xABCD #define X2 0x1234 #define A0 0xE66D #define A1 0xDEEC #define A2 0x5 #define C 0xB可以看出数组a中的三个元素保存的就是线性同余方程中的常数 0x5DEECE66D的三个部分。注意的是a中每个元素是uint32_t类型,就是说4个字节,实际上我们把0x5DEECE66D拆成三个部分(0x5、0xDEEC、0xE66D),每个部分最多只占用了2个字节。另外要注意的是宏定义中的X0和前文(上一节)中的X0语义是不同的,这里的X0表示的是数组X默认初始化时的第一个元素的内容,而上一节中提到的X0表示的是第一个随机数,相当于本节中的X2*2^32 + X1*2^16 + X0。
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 N 16 #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])。不要混淆。
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()采用的方案)。
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 m Xn+1 = ((aXn) mod m + c mod m) mod m Xn+1 = ((aXn) mod m + c) mod m
#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位。
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);
至此一次多项式相关的运算计数完毕,接下来是2^16次多项式的运算。
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]);倒着读吧,先看x[0],它保存的应该是一次多项式的结果。
标签:drand48 lrand48 伪随机数生成算法 随机数种子 redis源码
原文地址:http://blog.csdn.net/guodongxiaren/article/details/44885725