随机数漫谈
文章目录
略坑的 rand
下午在水群的时候无意看见一位学弟的提问,为什么自己写的C语言程序中前后两次使用rand获取的随机数是相同的。代码嘛具体记不全了,这里就贴一个简化版本吧(可能简化太多了)。
|
|
运行得到以下的结果:
|
|
熟悉C语言的朋友应该知道,C的标准库中的rand函数生成随机数(准确讲是伪随机数)使用的是线性同余法,只要指定的种子一样,不管你运行多少次程序,rand得到的数列是一模一样的。通常为了方便获取系统的时间戳当作种子,而时间戳的精度是到秒,很明显运行这几条指令所需的时间远远不及一秒。两个相同的种子,两次rand,理所当然地得到两个相同的“随机数”。解决方案很简单,只初始化一次种子即可。也有群友提出了其它方案,在两个srand中间插入sleep函数,这样完全也可以。
既然提到了随机数,我便开始想别的东西,让我产生随机数我会怎么整呢?由于我是忠实的Linux党,直接从 /dev/random 或 /dev/urandom 读出随机数不好嘛。可能使用的流程会稍稍变复杂一丢丢,打开文件,从中读取若干字节,但我觉得完全没有什么不妥。关于以上提到的两个伪设备,又有很多能扯的地方。有一篇文章很棒,至少讲清了两者的区别还有很多玄学问题:/dev/urandom 不得不说的故事。
Windows 下自然有密码学专用的随机数产生函数,不过以 Win32 API 的尿性,没个一堆的参数你别想用它。这里就不提它了,因为还有更好玩的东西。
硬件随机数 RDRAND
之前无意中见到近些年的 CPU 支持硬件随机数指令,Intel 是第三代酷睿架构(IvyBridge),而 AMD 是从2015年之后的产品。这条指令是 RDRAND。调用这个指令只会修改 rax/eax,从理论上讲比那些软件实现的不知道高到哪里去了。
但是有一个很尴尬的问题,该如何调用该指令呢?直接写汇编再编译或者内联汇编都是可行的,该指令的内联汇编看起来是比较复杂的(其实是我比较菜)。后发较新的 GCC 现有现成的库可以使用,那为什么不调库呢?写了一个 demo 如下:
|
|
这样看起来十分简单是不是,根据产生的随机数的长度不同,共有三个不同的函数(省略了很多的 GCC 的特有标识,以便观察):
|
|
但是编译却遇到一些问题:
|
|
看着有点懵,直接问 google,找到一个类似的,只要指定处理器架构就好了(当年天真的以为只有 SIMD 指令和架构相关,太 Native 了!!!),我编译用的指令为:
|
|
由于我的处理器是六代酷睿,就指定了 skylake,其实指定为 lvybridge 之后的版本都是可以滴。运行,结果如下:
|
|
算成成功了。突然又有一个大胆的想法,把这个程序丢在不支持的处理器上运行会怎样呢,好奇ing。。。
越看这个硬件随机数感觉越厉害,原理大致是利用电阻热噪声取得硬件真随机数。但为什么 C 的标准库还坚持用那么 Low 的实现呢。旧硬件不支持,算是吧。又 baidu 了一会,发现 AMD 一直在这个指令上翻车。先是老 APU A6-6310 只要从休眠或睡眠状态唤醒,产生的随机数就不那么随机了,不但如此还会导致无法再次进入休眠或睡眠状态;后有 Ryzen 3000 系列坚持 0xffffffff 是随机度最高的,质量最好的随机数。
额,都是 AMD 的锅,我们天天 Yes 支持你也要抓紧修复 Bug 啊。可见硬件随机数发生器也是存在不少问题的,Linux 内核不从 RDRAND 获取看来也是大有原因。最后一个问题,世界上真的有真随机数吗(陷入沉思),你怎么看呢?
参考
文章作者 Josephine
上次更新 2020-03-04