[英]Why is the new random library better than std::rand()?
所以我看到一個名為rand()的討論被認為是有害的 ,它主張使用隨機數生成的引擎分布范例而不是簡單的std::rand()
加模數范式。
但是,我想直接看到std::rand()
的失敗,所以我做了一個快速的實驗:
getRandNum_Old()
和getRandNum_New()
,它們分別使用std::rand()
和std::mt19937
+ std::uniform_int_distribution
生成0到5之間的隨機數。 結果如下:
[OLD WAY]
Spread
mean: 346.554406
std dev: 110.318361
Time Taken (ms)
mean: 6.662910
std dev: 0.366301
[NEW WAY]
Spread
mean: 350.346792
std dev: 110.449190
Time Taken (ms)
mean: 28.053907
std dev: 0.654964
令人驚訝的是,兩種方法的卷的總擴散是相同的。 即, std::mt19937
+ std::uniform_int_distribution
並不比簡單的std::rand()
+ %
“更加統一”。 我做的另一個觀察是新的比舊的方式慢了大約4倍。 總的來說,似乎我付出了巨大的速度成本,幾乎沒有質量上的提升。
我的實驗在某種程度上有缺陷嗎? 或者std::rand()
真的沒那么糟糕,甚至可能更好?
作為參考,這是我完整使用的代碼:
#include <cstdio>
#include <random>
#include <algorithm>
#include <chrono>
int getRandNum_Old() {
static bool init = false;
if (!init) {
std::srand(time(nullptr)); // Seed std::rand
init = true;
}
return std::rand() % 6;
}
int getRandNum_New() {
static bool init = false;
static std::random_device rd;
static std::mt19937 eng;
static std::uniform_int_distribution<int> dist(0,5);
if (!init) {
eng.seed(rd()); // Seed random engine
init = true;
}
return dist(eng);
}
template <typename T>
double mean(T* data, int n) {
double m = 0;
std::for_each(data, data+n, [&](T x){ m += x; });
m /= n;
return m;
}
template <typename T>
double stdDev(T* data, int n) {
double m = mean(data, n);
double sd = 0.0;
std::for_each(data, data+n, [&](T x){ sd += ((x-m) * (x-m)); });
sd /= n;
sd = sqrt(sd);
return sd;
}
int main() {
const int N = 960000; // Number of trials
const int M = 1000; // Number of simulations
const int D = 6; // Num sides on die
/* Do the things the "old" way (blech) */
int freqList_Old[D];
double stdDevList_Old[M];
double timeTakenList_Old[M];
for (int j = 0; j < M; j++) {
auto start = std::chrono::high_resolution_clock::now();
std::fill_n(freqList_Old, D, 0);
for (int i = 0; i < N; i++) {
int roll = getRandNum_Old();
freqList_Old[roll] += 1;
}
stdDevList_Old[j] = stdDev(freqList_Old, D);
auto end = std::chrono::high_resolution_clock::now();
auto dur = std::chrono::duration_cast<std::chrono::microseconds>(end-start);
double timeTaken = dur.count() / 1000.0;
timeTakenList_Old[j] = timeTaken;
}
/* Do the things the cool new way! */
int freqList_New[D];
double stdDevList_New[M];
double timeTakenList_New[M];
for (int j = 0; j < M; j++) {
auto start = std::chrono::high_resolution_clock::now();
std::fill_n(freqList_New, D, 0);
for (int i = 0; i < N; i++) {
int roll = getRandNum_New();
freqList_New[roll] += 1;
}
stdDevList_New[j] = stdDev(freqList_New, D);
auto end = std::chrono::high_resolution_clock::now();
auto dur = std::chrono::duration_cast<std::chrono::microseconds>(end-start);
double timeTaken = dur.count() / 1000.0;
timeTakenList_New[j] = timeTaken;
}
/* Display Results */
printf("[OLD WAY]\n");
printf("Spread\n");
printf(" mean: %.6f\n", mean(stdDevList_Old, M));
printf(" std dev: %.6f\n", stdDev(stdDevList_Old, M));
printf("Time Taken (ms)\n");
printf(" mean: %.6f\n", mean(timeTakenList_Old, M));
printf(" std dev: %.6f\n", stdDev(timeTakenList_Old, M));
printf("\n");
printf("[NEW WAY]\n");
printf("Spread\n");
printf(" mean: %.6f\n", mean(stdDevList_New, M));
printf(" std dev: %.6f\n", stdDev(stdDevList_New, M));
printf("Time Taken (ms)\n");
printf(" mean: %.6f\n", mean(timeTakenList_New, M));
printf(" std dev: %.6f\n", stdDev(timeTakenList_New, M));
}
幾乎任何“舊” rand()
使用LCG ; 雖然它們通常不是最好的發電機,但通常你不會在這樣的基本測試中看到它們失敗 - 即使是最差的PRNG,平均值和標准偏差通常也是正確的。
“壞”的常見缺陷 - 但很常見 - rand()
實現是:
RAND_MAX
; 但是,這些都不是特定於rand()
的API。 一個特定的實現可以把后面的xorshift家庭發生器srand
/ rand
和,algoritmically來說,獲得具有無接口的變化,藝術PRNG的狀態,所以像你這樣會顯示在輸出中的任何弱點的一個沒有測試。
實際上, rand()
的實際問題原則上並沒有太多的實現,但是:
RAND_MAX
僅為32767.然而,這不容易改變,因為它會破壞與過去的兼容性 - 使用srand
和固定種子進行可重復模擬的人不會太高興(事實上) ,IIRC上述實現可以追溯到Microsoft C早期版本 - 甚至是八十年代中期的Lattice C); 簡單的界面; rand()
為整個程序提供具有全局狀態的單個生成器。 雖然這對於許多簡單的用例來說非常好(並且實際上非常方便)但它會帶來問題:
最后, rand
的事態:
time(NULL)
不是,因為它不夠精細,而且經常 - 認為沒有RTC的嵌入式設備 - 甚至不夠隨機)。 因此新的<random>
標頭試圖修復這個混亂,提供以下算法:
...以及默認的random_device
以及播種它們。
現在,如果你問我,我會也喜歡上了一個簡單的API建立在對“易”除此之外,“猜一個數字”的情況下(類似Python不如何提供“復雜”的API,而且瑣碎random.randint
&Co。使用全球預播種PRNG為我們簡單的人們,他們不想淹沒在隨機設備/引擎/適配器/無論random.randint
我們想要為賓果卡提取數字,但是這是真的你可以通過當前的設施輕松地自己構建它(雖然在一個簡單的API上構建“完整的”API是不可能的)。
最后,為了回到你的性能比較:正如其他人所指出的那樣,你正在將快速LCG與較慢(但通常被認為質量較好)的Mersenne Twister進行比較; 如果您對LCG的質量沒問題,可以使用std::minstd_rand
而不是std::mt19937
。
實際上,在調整函數以使用std::minstd_rand
並避免無用的靜態變量進行初始化之后
int getRandNum_New() {
static std::minstd_rand eng{std::random_device{}()};
static std::uniform_int_distribution<int> dist{0, 5};
return dist(eng);
}
我得到9毫秒(舊)對比21毫秒(新); 最后,如果我擺脫dist
(與經典的模運算符相比,它處理輸出范圍的分布偏差而不是輸入范圍的多倍)並返回到你在getRandNum_Old()
中所做的事情
int getRandNum_New() {
static std::minstd_rand eng{std::random_device{}()};
return eng() % 6;
}
我把它降低到6毫秒(因此,快30%),可能是因為,與調用rand()
, std::minstd_rand
更容易內聯。
順便說一句,我使用手動(但非常符合標准庫接口) XorShift64*
進行了相同的測試,它比rand()
快了2.3倍(3.68 ms vs 8.61 ms); 因為,不像梅森倍捻機和各種提供的LCG,它通過目前的隨機性測試套件出色 ,它的速度極快,它使你想知道為什么它不是在標准庫中包含的呢。
如果您使用大於5的范圍重復實驗,那么您可能會看到不同的結果。 當您的范圍明顯小於RAND_MAX
,大多數應用程序都不會出現問題。
例如,如果我們的RAND_MAX
為25,則rand() % 5
將生成具有以下頻率的數字:
0: 6
1: 5
2: 5
3: 5
4: 5
由於RAND_MAX
保證大於32767並且最不可能和最可能之間的頻率差異僅為1,對於小數字,對於大多數用例,分布幾乎足夠隨機。
首先,令人驚訝的是,答案會根據你使用的隨機數而改變。 如果要驅動一個隨機的背景顏色轉換器,使用rand()就可以了。 如果您使用隨機數創建隨機撲克手或加密安全密鑰,那么它就不好了。
可預測性:序列012345012345012345012345 ...將為您的樣本中的每個數字提供均勻分布,但顯然不是隨機的。 對於序列是隨機的,n + 1的值不能通過n的值(或者甚至是n,n-1,n-2,n-3等的值)容易地預測。顯然,重復序列相同數字的數字是簡並的情況,但是可以對任何線性同余生成器生成的序列進行分析; 如果您使用來自公共庫的常見LCG的默認開箱即用設置,惡意的人可以“完全打破序列”而不需要太多努力。 在過去,幾家在線賭場(以及一些實體賭場)因使用不良隨機數發生器的機器而遭受損失。 即使是應該知道更好的人也會被抓住; 已經證明來自幾個制造商的TPM芯片比由密鑰生成參數做出的不良選擇所預測的密鑰的比特長度更容易破解。
分布:正如視頻中所提到的,采用100的模數(或任何不能均勻分割為序列長度的值)將保證某些結果至少比其他結果更有可能。 在32767個可能的起始值為100的宇宙中,數字0到66將比值67到99更頻繁地出現328/327(0.3%); 可能為攻擊者提供優勢的因素。
正確的答案是:它取決於你的意思是“更好”。
13年前,“新” <random>
引擎被引入C ++,所以它們並不是真正的新東西。 C庫rand()
是在幾十年前推出的,並且在那個時代對於任何數量的東西非常有用。
C ++標准庫提供了三類隨機數生成器引擎:線性同余(其中rand()
是一個例子),Lagged Fibonacci和Mersenne Twister。 每個班級都有權衡,每個班級在某些方面都是“最好的”。 例如,LCG具有非常小的狀態,如果選擇了正確的參數,則在現代桌面處理器上相當快。 LFG具有更大的狀態並且僅使用存儲器提取和加法操作,因此在缺少專用數學硬件的嵌入式系統和微控制器上非常快。 MTG具有巨大的狀態並且很慢,但是可以具有非常大的非重復序列,具有優異的光譜特性。
如果所提供的生成器都不足以滿足您的特定用途,則C ++標准庫還為硬件生成器或您自己的自定義引擎提供接口。 沒有一個發生器是獨立使用的:它們的預期用途是通過一個分配對象,它提供一個具有特定概率分布函數的隨機序列。
<random>
over rand()
另一個優點是rand()
使用全局狀態,不是可重入或線程安全的,並且每個進程允許一個實例。 如果您需要細粒度控制或可預測性(即能夠重現給定RNG種子狀態的錯誤),那么rand()
是無用的。 <random>
生成器是本地實例化的,具有可序列化(和可恢復)狀態。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.