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 = 0x5DEECE66D
c = 0xB
m = 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 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。

对外函数

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 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])。不要混淆。

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 m
Xn+1 = ((aXn) mod m + c mod m) mod m
Xn+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]的那段代码,就清楚多了吧,重点是考虑进位。

时间: 2024-10-05 03:13:23

Redis源码中看伪随机数生成算法的相关文章

Redis源码中探秘SHA-1算法原理及其编程实现

导读 SHA-1算法是第一代"安全散列算法"的缩写,其本质就是一个Hash算法.SHA系列标准主要用于数字签名,生成消息摘要,曾被认为是MD5算法的后继者.如今SHA家族已经出现了5个算法.Redis使用的是SHA-1,它能将一个最大2^64比特的消息,转换成一串160位的消息摘要,并能保证任何两组不同的消息产生的消息摘要是不同的.虽然SHA1于早年间也传出了破解之道,但作为SHA家族的第一代算法,对我们仍然很具有学习价值和指导意义. SHA-1算法的详细内容可以参考官方的RFC:ht

Redis源码分析(二十三)--- CRC循环冗余算法和RAND随机数算法

今天开始研究Redis源码中的一些工具类的代码实现,工具类在任何语言中,实现的算法原理应该都是一样的,所以可以借此机会学习一下一些比较经典的算法.比如说我今天看的Crc循环冗余校验算法和rand随机数产生算法. CRC算法全称循环冗余校验算法.CRC校验的基本思想是利用线性编码理论,在发送端根据要传送的k位二进制码序列,以一定的规则产生一个校验用的监督码(既CRC码)r位,并附在信息后边,构成一个新的二进制码序列数共(k+r)位,最后发送出去.在接收端, 则根据信息码和CRC码之间所遵循的规则进

redis源码分析4---结构体---跳跃表

redis源码分析4---结构体---跳跃表 跳跃表是一种有序的数据结构,他通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的: 跳跃表支持平均O(logN),最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点.性能上和平衡树媲美,因为事先简单,常用来代替平衡树. 在redis中,只在两个地方使用了跳跃表,一个是实现有序集合键,另一个是在集群节点中用作内部数据结构. 1 跳跃表节点 1.1 层 层的数量越多,访问其他节点的速度越快: 1.2 前进指针 遍历举例

Redis源码中的CRC校验码(crc16、crc64)原理浅析

在阅读Redis源码的时候,看到了两个文件:crc16.c.crc64.c.下面我抛砖引玉,简析一下原理. CRC即循环冗余校验码,是信息系统中一种常见的检错码.大学课程中的"计算机网络"."计算机组成"等课程中都有提及.我们可能都了解它的数学原理,在试卷上手工计算一个CRC校验码,并不是难事.但是计算机不是人,现实世界中的数学原理需要转化为计算机算法才能实现目的.实际上作为计算机专业背景人并不会经常使用或接触到CRC的计算机算法实现的原理,通常是电子学科背景的人士

redis源码分析(1)--makefile和目录结构分析

一.redis源码编译 redis可以直接在官网下载(本文使用版本 3.0.7):https://redis.io/download 安装: $ tar xzf redis-3.0.7.tar.gz $ cd redis-3.0.7 $ make make执行以后主要编译产物在src/redis-server src/redis-cli 如果想把redis-server直接install到可执行目录/usr/local/bin,还需要执行: $ make install Run Redis wi

redis源码分析3---结构体---字典

redis源码分析3---结构体---字典 字典,简单来说就是一种用于保存键值对的抽象数据结构: 注意,字典中每个键都是独一无二的:在redis中,内部的redis的数据库就是使用字典作为底层实现的: 1 字典的实现 在redis中,字典是使用哈希表作为底层实现的,一个hash表里面可以有多个hash表节点,而每个hash表节点就保存了字典中的一个键值对: hash表定义 table属性是一个数组,数组中的每个元素都是一个指向dictEntry结构的指针,每个dictEntry结构保存着一个键值

Redis源码研究—哈希表

Redis源码研究-哈希表 Category: NoSQL数据库 View: 10,980 Author: Dong 作者:Dong | 新浪微博:西成懂 | 可以转载, 但必须以超链接形式标明文章原始出处和作者信息及版权声明 网址:http://dongxicheng.org/nosql/redis-code-hashtable/ 本博客的文章集合:http://dongxicheng.org/recommend/ 本博客微信公共账号:hadoop123(微信号为:hadoop-123),分享

redis源码修改之zincrby,hincrby命令

在项目中大量使用zincrby命令,原因就是要统计日志中某个指标的计数值,且需要按顺序返回topn. 正常来说,一个指标调用一次zincrby(zincrby default:type 1 typeA) 就可以正常工作. 实际情况是由于日志生成的太快,redis cpu利用率经常100%,而且还丢数据. 是否可以一次性增加多次指标的累计值,比如zincrby default:type 1 typeA 1 typeB 1 typeC ...,这样将多次通信压缩到一次通信中,肯定能提高处理能力. 无

Redis源码学习-Lua脚本

Redis源码学习-Lua脚本 1.Sublime Text配置 我是在Win7下,用Sublime Text + Cygwin开发的,配置方法请参考<Sublime Text 3下C/C++开发环境搭建>. 要注意的是:在Cygwin中安装Lua解析器后,SublimeClang插件就能识别出可饮用的Lua头文件了,因为Build System中我们已经配置过"-I", "D:\\cygwin64\\usr\\include",而新安装的Lua头文件会