srand 源码分析(非专业)
( 原文地址:https://0xffff.one/d/262-srand-yuan-ma-fen-xi-fei-zhuan-ye )
这篇帖子源于群里的一个提问:
1 | 为什么srand((unsigned int)time())的种子要设置成unsigned? |
最直接的回答就是因为在头文件里srand的函数定义里规定了参数类型是unsigned int吧,于是就去翻了一下glibc的源码。
Warnning: 这是一篇偏技术的文章,会涉及比较多的代码!
srand()是在stdlib库里的,所以先看stdlib/stdlib.h这个文件,在451~456行的地方(不同版本的库可能会不一样)可以找到srand()/rand()的定义:
1 | /* Return a random integer between 0 and RAND_MAX inclusive. */ |
从定义就可以直接看出srand()传进去的seed是规定unsigned int类型的了。
然后找一下这两个函数的实现,发现没有找到,然后在stdlib.h的395~405行看到这样的一段东西:
1 | /* These are the functions that actually do things. The `random', `srandom', |
大概意思是srand/rand是假的,srandom/random才是真正干事情的。用gdb跑了一下,发现真的是这样(图片大概意思是在call了srand函数后,马上就有一条jmp指令跳转到srandom函数了,rand函数也类似):
在stdlib/random.c里面可以找到函数的实现:
1 | void __srandom (unsigned int x) |
函数里面用到了锁,所以有用到多线程的怀疑(?),真正执行的是一个叫 __srandom_r 的递归函数,在文件stdlib/random_r.c里面可以找到(前方高能):
1 | /* Initialize the random number generator based on the given seed. If the |
首先,这个函数的作用是通过一个随机数种子生成一串随机数,存到buf->state(通过参数传入,在函数中是局部变量state)中。这一串数(state)的第一个为传入的种子(seed)。比较有意思的是seed是不能为0的,如果是0的话会强行变成1:
1 | /* We must make sure the seed is not 0. Take arbitrarily 1 in this case. */ |
可以用一下的一段代码进行一下测试:
1 | /* test.c */ |
至于为什么不能是0可以接着往下看,函数主要作用的是这个for循环:
1 | for (i = 1; i < kc; ++i) |
如果seed是0的话,整个随机数序列就都是0了。这个循环做的其实就是一个递归式的运算
1 | state[i] = (16807 * state[i - 1]) % 2147483647 |
但是就拆分成了几行代码(运算结果应该是一样的,但是却拆分成hi和lo两个部分计算后再合成word),估计就是考虑到提升速度的方面的:一个猜想是取余运算其实是用除法来做的,当数比较大的时候会花费比较多的时间,所以就拆分成两个较小的数来进行运算;另一个猜想是因为在汇编层面来看,进行除法运算后会产生两个结果,商和余数,分别放在两个不同的寄存器,所以回到C的层面看,做除法的话就是拿商的结果,做取余的话就是拿余数的结果,所以word / 127773和word % 127773其实只用进行一次除法运算就可以同时得到hi和lo的结果了。
由srand的实现可以看出C语言的随机数是伪随机数,只要种子确定了,根据上面的递归式生成的随机数序列就是确定的了。
对比一下C11的random,翻了一下C11的源码(看不懂- -,但是发现了/dev/urandom这个东西),发现C++11是通过随机熵来实现随机数的,(估计会比递归式的更好?)