簡體   English   中英

為什么新的隨機庫比std :: rand()更好?

[英]Why is the new random library better than std::rand()?

所以我看到一個名為rand()的討論被認為是有害的 ,它主張使用隨機數生成的引擎分布范例而不是簡單的std::rand()加模數范式。

但是,我想直接看到std::rand()的失敗,所以我做了一個快速的實驗:

  1. 基本上,我編寫了2個函數getRandNum_Old()getRandNum_New() ,它們分別使用std::rand()std::mt19937 + std::uniform_int_distribution生成0到5之間的隨機數。
  2. 然后我使用“舊”方式生成了960,000(可被6整除)的隨機數,並記錄了數字0-5的頻率。 然后我計算了這些頻率的標准偏差。 我正在尋找的是盡可能低的標准偏差,因為如果分布是真正均勻的那樣會發生什么。
  3. 我運行了1000次模擬並記錄了每次模擬的標准偏差。 我還記錄了它花費的時間,以毫秒為單位。
  4. 之后,我再次完全相同,但這次生成隨機數字的“新”方式。
  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 ;
  • 連續提取之間的一些相關性(通常,LCG產生有限數量的超平面上的數字,盡管這可以以某種方式減輕)。

但是,這些都不是特定於rand()的API。 一個特定的實現可以把后面的xorshift家庭發生器srand / rand和,algoritmically來說,獲得具有無接口的變化,藝術PRNG的狀態,所以像你這樣會顯示在輸出中的任何弱點的一個沒有測試。


實際上, rand()的實際問題原則上並沒有太多的實現但是:

  • 向后兼容; 許多當前的實現使用次優的生成器,通常具有錯誤選擇的參數; 一個臭名昭着的例子是Visual C ++,其RAND_MAX僅為32767.然而,這不容易改變,因為它會破壞與過去的兼容性 - 使用srand和固定種子進行可重復模擬的人不會太高興(事實上) ,IIRC上述實現可以追溯到Microsoft C早期版本 - 甚至是八十年代中期的Lattice C);
  • 簡單的界面; rand()為整個程序提供具有全局狀態的單個生成器。 雖然這對於許多簡單的用例來說非常好(並且實際上非常方便)但它會帶來問題:

    • 使用多線程代碼:為了修復它,你需要一個全局互斥體 - 它會無緣無故地減慢一切消除任何可重復性的機會,因為調用序列本身就是隨機的 - 或線程本地狀態; 最后一個已經被幾個實現采用(特別是Visual C ++);
    • 如果您想要一個“私有”,可重現的序列到您的程序的特定模塊中,不會影響全局狀態。

最后, rand的事態:

  • 沒有指定實際的實現(C標准只提供了一個示例實現),因此任何旨在跨不同編譯器生成可重現輸出(或期望某種已知質量的PRNG)的程序必須使用自己的生成器;
  • 沒有提供任何跨平台的方法來獲得合適的種子( 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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM