C++11隨機數的正確打開方式


C++11隨機數的正確打開方式

在C++11之前,現有的隨機數函數都存在一個問題:在利用循環多次獲取隨機數時,如果程序運行過快或者使用了多線程等方法,srand((unsigned)time(null))這樣的設置當前系統時間為種子的方法每次返回的隨機數都是一樣的。而C++11中提供了真隨機數做種子的方法來解決這一問題。

By the way,2019年了,我見過的編譯器都不需要特殊指定使用的是C++11的新特征了

random_device

標准庫提供了一個非確定性隨機數生成設備.在Linux的實現中,是讀取/dev/urandom設備;Windows的實現是用rand_s,使用的是操作系統來生成加密安全的偽隨機數

注意,urandom實際上也是一種偽隨機數,具體見下一節

random_device提供()操作符,用來返回一個min()到max()之間的一個數字.

注意這里的min()和max()都是只能看不能改的。

random_device一般只用來作為其他偽隨機數算法的種子,原因有三:

  1. random_device的最大值和最小值不能修改,當然可以通過取模的方式獲得想要的范圍的數,但是畢竟不太優雅
  2. 當熵池用盡后,許多random_device的具體實現的性能會急速下降(原話是:"the performance of many implementations of random_device degrades sharply once the entropy pool is exhausted. For practical use random_device is generally only used to seed a PRNG such as mt19937"(來源:https://stackoverflow.com/questions/39288595/why-not-just-use-random-device)。但是這一點也有爭議,在linux下random_device的實現其實是std::fopen("/dev/urandom"),有人說urandom在熵池耗盡之后輸出的隨機數是低質量的,但也有人說不是(https://blog.csdn.net/F8qG7f9YD02Pe/article/details/89880266),我沒有對它做過多研究,但是下一點是毋庸置疑的
  3. 多次調用random_device要花費比其他偽隨機數算法更多的時間。在Linux中,正如上文所說,每次調用random_device都需要讀urandom這個文件再關閉,而在WIndom中我們需要調用操作系統的API,再銷毀實例化對象,這個時間花費顯然比設置好種子就能一直產生的其他偽隨機數算法要慢得多。

所以,我們一般將random_device只用作種子。

有關於熵池的內容見下一節。划重點:“產生真隨機數依賴於熵池中的噪聲資源。如果熵池資源耗盡,就需要等到收集足夠多的環境噪聲時,才能繼續產生新的隨機數。”

[轉載]Linux 內核熵池與 /dev/urandom

原文地址:http://www.codebelief.com/article/2017/10/linux-entropy-pool-and-dev-urandom/

從計算機隨機數談起

我們知道,計算機是一個可預測的系統,因此不可能通過算法來產生真正的隨機數。在計算機中,所謂的隨機數通常都是偽隨機數,就是通過隨機算法計算出來的,可以被近似看作隨機數的數值。常見的隨機數算法有線性同余法(Linear Congruential Generator)、梅森旋轉法(Mersenne twister)等,前者是大部分編譯器采用的算法,隨機性相對差一些;而后者是更為優秀的隨機算法,隨機性好,被 Python、Ruby 等語言用作默認的隨機算法。

但是,隨機算法的缺陷也是很明顯的。一方面,隨機性越好的算法計算復雜度越大;另一方面,即使隨機性再好,也無法與真正的隨機數相媲美。因此,產生真正的隨機數是最理想的方法。

Linux 內核熵池

Linux 內核采用熵來描述數據的隨機性。在物理學中,熵(entropy)是一個描述系統混亂程度的物理量,熵越大說明系統越無序、越混亂,不確定性越大。

雖然計算機本身可預測,但計算機的運行環境中充滿了各種不可預知的噪聲,例如來自設備驅動的噪聲、隨機的鼠標點擊間隔、硬件設備發生中斷的時間等等。

Linux 系統維護了一個專門用於收集上述噪聲的熵池(entropy pool),這些噪聲將被用於產生真正的隨機數。

需要注意的是,產生真隨機數依賴於熵池中的噪聲資源。如果熵池資源耗盡,就需要等到收集足夠多的環境噪聲時,才能繼續產生新的隨機數。

/dev/urandom

Linux 提供了內核隨機數生成器的接口,即字符設備/dev/random,該字符設備用於生成高質量的隨機數,它會確保熵池資源足夠時才生成隨機數。如上面所說,當熵池為空時,對/dev/random 的讀取操作將會阻塞,直到收集足夠的噪聲為止。

使用/dev/random 來生成隨機數,很可能導致應用被阻塞。幸運的是,Linux 中還有另一個隨機數生成器/dev/urandom,該字符設備是/dev/random 的非阻塞版本,准確說它是一個偽隨機數生成器,它的隨機數種子來自於熵池,不過即使熵池為空,/dev/urandom 仍然能產生隨機數。

Linux 的 man(4)手冊中這么寫道:

/dev/random 是一個遺留下來的接口,在所有使用場景中,/dev/urandom 更受歡迎並且能滿足要求。

/dev/random 接口遺留下來的原因主要是早期/dev/urandom 所采用的密碼算法未被大家信任,但現在,/dev/urandom 已經被廣泛的采用了。

解決由/dev/random 引起的阻塞

目前,仍有一些應用使用/dev/random 來生成隨機數,這些應用在運行時,可能由於熵池耗盡而阻塞。例如 tomcat7.0 以上的版本,依賴於該生成器來生成隨機數,可能啟動時便沒有反應,實際上是等待熵池收集噪聲。此外,strongSwan 生成 CA 時使用的 ipsec pki 命令也可能因此而阻塞。

解決該問題可以通過安裝 havged 程序來解決。haveged 是一個簡單易用的不可預測隨機數生成器,基於 HAVEGE 算法。haveged 可以解決在某些情況下,系統熵過低的問題。

參考 man 手冊

random(4) - Linux manual page

常用的隨機數算法

上面我們說了,random_device一般只用來做種子,C++11中常用來做為偽隨機數算法的有以下幾種:

  • linear_congruential_engine線性同余法,這種速度最快、最常用
  • mersenne_twister_engine梅森旋轉法,這種生成的隨機數質量比較高
  • substract_with_carry_engine滯后Fibonacci

但是這些類可以直接使用嗎?這是不行的,這三個都是模板類,只是定義了接口,需要我們自己將它實例化。但是顯然每個人實例化一次是不現實的,所以C++11中預先實現了一些給我們,我們只需要直接創建這些類的對象就好了。

img

還有一個default_random_engine的類。它是一個實例化的類。之所以不歸入那三種算法,是因為它的實現是由編譯器廠家決定的,有的可能用linear_congruential_engine實現,有的可能用mersenne_twister_engine實現。這種現象在C/C++中見多了。不過,對於其他的類,C++11是有明確規定用哪種算法和參數實現的。

對於default_random_engine來說,其產生的隨機數范圍是在[min(), max()]之間,其中min()和max()為它的兩個成員函數,是閉區間。

來源:https://blog.csdn.net/luotuo44/article/details/33690179

在C++11里面,把這些隨機數生成器叫做引擎(engines)

注意,random_device也是一種隨機數引擎

雖然和每次都直接使用random_device相比,使用這些算法大大減少了多次生成隨機數時的平均時間和空間花費,但是這些算法也存在問題,就是他們產生的范圍還是太大了,如果我們需要特定范圍下的隨機數,依然需要取模。為了解決這一問題,我們要引出隨機分布模板類。

隨機分布模板類

常見的隨機分布模板類

均勻分布:

​ uniform_int_distribution 整數均勻分布

​ uniform_real_distribution 浮點數均勻分布

注意,uniform_int_distribution的隨機數的范圍不是半開范圍[ ),而是[ ],對於uniform_real_distribution卻是半開范圍[ )。

伯努利類型分布:(僅有yes/no兩種結果,概率一個p,一個1-p)

​ bernoulli_distribution 伯努利分布

​ binomial_distribution 二項分布

​ geometry_distribution 幾何分布

​ negative_biomial_distribution 負二項分布

Rate-based distributions:

​ poisson_distribution 泊松分布

​ exponential_distribution指數分布

​ gamma_distribution 伽馬分布

​ weibull_distribution 威布爾分布

​ extreme_value_distribution 極值分布

正態分布相關:

​ normal_distribution 正態分布

​ chi_squared_distribution卡方分布

​ cauchy_distribution 柯西分布

​ fisher_f_distribution 費歇爾F分布

​ student_t_distribution t分布

分段分布相關:

​ discrete_distribution離散分布

​ piecewise_constant_distribution分段常數分布

​ piecewise_linear_distribution分段線性分布

這些模板類都是定義好了的、可以直接使用的。

這些概率分布函數都是有參數的,在類的構造函數中把參數傳進去即可。我們最常用的還是均勻分布,這里以 uniform_int_distribution為例介紹以下如何使用這些算法:

#include <random>
#include<iostream>
using namespace std;//要是不使用std名字空間,下面的就都需要加std::
void formData(){
	random_device sd;//生成random_device對象sd做種子
    minstd_rand linearRan(sd());//使用種子初始化linear_congruential_engine對象,為的是使用它來做我們下面隨機分布的種子以輸出多個隨機分布.注意這里要使用()操作符,因為minst_rand()接受的是一個值(你用srand也是給出這樣的一個值)
    uniform_int_distribution<int>dis1(0,1);//生成01序列
    for(int i=0;i<100;i++){
        cout<<dis1(linearRan)<<endl;//使用linear engine做種子,注意這里傳入的不是一個值而是一個引擎:【隨機分布函數需要傳入一個隨機數引擎作為參數,其實random_device也是一個引擎,這里把sd傳入也不會出錯】
    }
}
int main(){
    formData();
}

總結

還是有點繞的。總之,要得到不止一個一個我們最常需要的、符合一定分布規律的且隨機質量較高的隨機數,我們要做的是:

  1. 定義random_device對象
  2. 選擇隨機引擎(默認、線性、梅森、斐波那契)的實現類,將random_device的隨機結果傳入作為種子
  3. 選擇要的分布,創建分布對象,將引擎傳入作為種子,讓分布對象輸出隨機數。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM