Tại sao thư viện ngẫu nhiên mới tốt hơn std :: rand ()?


82

Vì vậy, tôi đã xem một cuộc nói chuyện có tên là rand () Được coi là có hại và nó ủng hộ việc sử dụng mô hình phân phối động cơ của việc tạo số ngẫu nhiên trên std::rand()mô hình mô đun cộng đơn giản .

Tuy nhiên, tôi muốn std::rand()tận mắt chứng kiến ​​những thất bại nên tôi đã thực hiện một thử nghiệm nhanh:

  1. Về cơ bản, tôi đã viết 2 hàm getRandNum_Old()getRandNum_New()điều đó tạo ra một số ngẫu nhiên từ 0 đến 5 bằng cách sử dụng std::rand()std::mt19937+ std::uniform_int_distributiontương ứng.
  2. Sau đó, tôi tạo ra 960.000 (chia hết cho 6) số ngẫu nhiên theo cách "cũ" và ghi lại tần số của các số 0-5. Sau đó, tôi tính toán độ lệch chuẩn của các tần số này. Những gì tôi đang tìm kiếm là độ lệch chuẩn càng thấp càng tốt vì đó là điều sẽ xảy ra nếu phân phối thực sự đồng đều.
  3. Tôi đã chạy mô phỏng đó 1000 lần và ghi lại độ lệch chuẩn cho mỗi mô phỏng. Tôi cũng ghi lại thời gian tính bằng mili giây.
  4. Sau đó, tôi làm lại chính xác như vậy nhưng lần này tạo ra các số ngẫu nhiên theo cách "mới".
  5. Cuối cùng, tôi tính toán giá trị trung bình và độ lệch chuẩn của danh sách độ lệch chuẩn cho cả cách cũ và cách mới, giá trị trung bình và độ lệch chuẩn cho danh sách thời gian được thực hiện cho cả cách cũ và cách mới.

Đây là kết quả:

[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

Điều đáng ngạc nhiên là độ rải tổng hợp của các cuộn là như nhau cho cả hai phương pháp. Tức là std::mt19937+ std::uniform_int_distributionkhông "đồng nhất" hơn std::rand()+ đơn giản %. Một quan sát khác mà tôi thực hiện là cách mới chậm hơn khoảng 4 lần so với cách cũ. Nhìn chung, có vẻ như tôi đã phải trả một chi phí lớn về tốc độ mà hầu như không đạt được chất lượng.

Thử nghiệm của tôi có sai sót theo một cách nào đó không? Hay std::rand()thực sự là không tệ, và thậm chí có thể tốt hơn?

Để tham khảo, đây là mã tôi đã sử dụng toàn bộ:

#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));
}

32
Đó là lý do tại sao lời khuyên này tồn tại. Nếu bạn không biết cách kiểm tra RNG có đủ entropy hay không hoặc liệu nó có quan trọng đối với chương trình của bạn hay không thì bạn nên cho rằng std :: rand () không đủ tốt. en.wikipedia.org/wiki/Entropy_(computing)
Hans Passant

4
Điểm mấu chốt của việc liệu rand()có đủ tốt hay không phần lớn phụ thuộc vào việc bạn đang sử dụng tập hợp các số ngẫu nhiên để làm gì. Nó bạn cần một kiểu phân phối ngẫu nhiên cụ thể, sau đó, tất nhiên việc triển khai thư viện sẽ tốt hơn. Nếu bạn chỉ cần các số ngẫu nhiên và không quan tâm đến "tính ngẫu nhiên" hoặc loại phân phối được tạo ra, thì rand()tốt. Kết hợp công cụ thích hợp với công việc trong tầm tay.
David C. Rankin

2
dupe có thể xảy ra: stackoverflow.com/questions/52869166/… Tôi chỉ không muốn đánh giá cái này, vì vậy tôi không thực sự bỏ phiếu.
bolov

18
for (i=0; i<k*n; i++) a[i]=i%n;tạo ra giá trị trung bình chính xác và độ lệch chuẩn giống như RNG tốt nhất hiện có. Nếu điều này đủ tốt cho ứng dụng của bạn, chỉ cần sử dụng trình tự này.
n. 'đại từ' m.

3
"độ lệch chuẩn càng thấp càng tốt" - không. Sai rồi. Bạn mong đợi các tần số sẽ khác một chút - về sqrt (tần số) là về những gì bạn mong đợi độ lệch chuẩn. "Bộ đếm tăng dần" mà nm tạo ra sẽ có sd thấp hơn nhiều (và là một rng rất xấu).
Martin Bonner ủng hộ Monica.

Câu trả lời:


106

Khá nhiều việc triển khai "cũ" rand()sử dụng LCG ; trong khi chúng nói chung không phải là máy phát điện tốt nhất, thông thường bạn sẽ không thấy chúng thất bại trong một bài kiểm tra cơ bản như vậy - trung bình và độ lệch chuẩn thường được đánh giá đúng ngay cả với PRNG tồi tệ nhất.

Các lỗi thường gặp khi triển khai "xấu" - nhưng đủ phổ biến - rand()là:

  • độ ngẫu nhiên thấp của các bit bậc thấp;
  • thời gian ngắn;
  • thấp RAND_MAX;
  • một số mối tương quan giữa các lần khai thác liên tiếp (nói chung, LCG tạo ra các con số nằm trên một số siêu máy bay có giới hạn, mặc dù điều này có thể được giảm thiểu bằng cách nào đó).

Tuy nhiên, không có cái nào trong số này là cụ thể cho API của rand(). Một triển khai cụ thể có thể đặt một trình tạo xorshift-family phía sau srand/ randvà, nói theo nghĩa lý, có được PRNG hiện đại mà không có thay đổi về giao diện, vì vậy không có thử nghiệm nào giống như thử nghiệm bạn đã làm sẽ cho thấy bất kỳ điểm yếu nào trong đầu ra.

Chỉnh sửa: @R. lưu ý một cách chính xác rằng rand/ srandinterface bị giới hạn bởi thực tế srandlấy một unsigned int, vì vậy bất kỳ trình tạo nào mà một triển khai có thể đặt sau chúng về bản chất đều bị giới hạn ở UINT_MAXcác hạt bắt đầu có thể có (và do đó, các chuỗi được tạo ra). Điều này thực sự đúng, mặc dù API có thể được mở rộng một cách đáng kể để thực srandhiện unsigned long longhoặc thêm một srand(unsigned char *, size_t)quá tải riêng .


Thật vậy, vấn đề thực tế rand()không nằm ở việc thực hiện nhiều về nguyên tắc mà là:

  • khả năng tương thích ngược; nhiều triển khai hiện tại sử dụng các bộ tạo không tối ưu, thường có các tham số được chọn sai; một ví dụ nổi tiếng là Visual C ++, có RAND_MAXchỉ số 32767. Tuy nhiên, điều này không thể thay đổi dễ dàng, vì nó sẽ phá vỡ khả năng tương thích với quá khứ - những người sử dụng srandhạt giống cố định để mô phỏng có thể tái tạo sẽ không quá vui (thực sự là IIRC việc triển khai nói trên quay trở lại với các phiên bản ban đầu của Microsoft C - hoặc thậm chí là Lattice C - từ giữa những năm tám mươi);
  • giao diện đơn giản; rand()cung cấp một trình tạo duy nhất với trạng thái chung cho toàn bộ chương trình. Mặc dù điều này hoàn toàn ổn (và thực sự khá tiện dụng) cho nhiều trường hợp sử dụng đơn giản, nhưng nó lại gây ra các vấn đề:

    • với mã đa luồng: để sửa nó, bạn cần một mutex toàn cục - thứ sẽ làm chậm mọi thứ mà không có lý do giết chết bất kỳ cơ hội lặp lại nào, vì chuỗi lệnh gọi trở thành ngẫu nhiên - hoặc trạng thái luồng cục bộ; cái cuối cùng này đã được chấp nhận bởi một số triển khai (đặc biệt là Visual C ++);
    • nếu bạn muốn một trình tự "riêng tư", có thể tái tạo vào một mô-đun cụ thể của chương trình của bạn mà không ảnh hưởng đến trạng thái chung.

Cuối cùng, randtình trạng của công việc:

  • không chỉ định một triển khai thực tế (tiêu chuẩn C chỉ cung cấp một triển khai mẫu), vì vậy bất kỳ chương trình nào nhằm tạo ra đầu ra có thể lặp lại (hoặc mong đợi một PRNG có chất lượng đã biết) trên các trình biên dịch khác nhau phải cuộn bộ tạo của riêng nó;
  • không cung cấp bất kỳ phương pháp đa nền tảng nào để có được một hạt giống tốt ( time(NULL)không phải, vì nó không đủ chi tiết và thường - nghĩ rằng các thiết bị nhúng không có RTC - thậm chí không đủ ngẫu nhiên).

Do đó <random>, tiêu đề mới , cố gắng khắc phục sự lộn xộn này cung cấp các thuật toán:

  • được chỉ định đầy đủ (vì vậy bạn có thể có đầu ra có thể tái tạo trình biên dịch chéo và các đặc tính được đảm bảo - ví dụ, phạm vi của bộ tạo);
  • nói chung có chất lượng hiện đại ( từ khi thư viện được thiết kế ; xem bên dưới);
  • được đóng gói trong các lớp (vì vậy không có trạng thái toàn cục nào bị ép buộc đối với bạn, điều này tránh hoàn toàn các vấn đề về phân luồng và không định vị);

... và một mặc định random_devicecũng để gieo chúng.

Bây giờ, nếu bạn hỏi tôi, tôi đã có thể thích cũng một API đơn giản được xây dựng trên này cho "dễ dàng", "đoán một số" trường hợp (tương tự như cách Python không cung cấp "phức tạp" API, mà còn là tầm thường random.randint& Co . sử dụng PRNG toàn cầu, được gieo sẵn hạt giống cho chúng tôi, những người không phức tạp, những người không muốn chết chìm trong các thiết bị / động cơ / bộ điều hợp ngẫu nhiên / bất cứ khi nào chúng tôi muốn trích xuất một số cho thẻ bingo), nhưng đúng là bạn có thể dễ tự xây dựng nó trên các cơ sở hiện tại (trong khi không thể xây dựng API "đầy đủ" trên một API đơn giản).


Cuối cùng, để quay lại so sánh hiệu suất của bạn: như những người khác đã chỉ định, bạn đang so sánh LCG nhanh với Mersenne Twister chậm hơn (nhưng thường được coi là chất lượng tốt hơn); nếu bạn đồng ý với chất lượng của LCG, bạn có thể sử dụng std::minstd_randthay thế std::mt19937.

Thật vậy, sau khi tinh chỉnh hàm của bạn để sử dụng std::minstd_randvà tránh các biến tĩnh vô ích để khởi tạo

int getRandNum_New() {
    static std::minstd_rand eng{std::random_device{}()};
    static std::uniform_int_distribution<int> dist{0, 5};
    return dist(eng);
}

Tôi nhận được 9 ms (cũ) so với 21 ms (mới); cuối cùng, nếu tôi loại bỏ dist(mà, so với toán tử mô-đun cổ điển, xử lý độ lệch phân phối cho phạm vi đầu ra không phải là nhiều của phạm vi đầu vào) và quay lại những gì bạn đang làm tronggetRandNum_Old()

int getRandNum_New() {
    static std::minstd_rand eng{std::random_device{}()};
    return eng() % 6;
}

Tôi nhận được nó xuống còn 6 mili giây (nhanh hơn 30%), có lẽ bởi vì, không giống như cuộc gọi tới rand(), std::minstd_randdễ dàng hơn trong dòng.


Thật tình cờ, tôi đã thực hiện cùng một bài kiểm tra bằng cách sử dụng cuộn tay (nhưng khá phù hợp với giao diện thư viện tiêu chuẩn) XorShift64*và nó nhanh hơn 2,3 lần rand()(3,68 ms so với 8,61 ms); do đó, không giống như Mersenne Twister và các LCG khác nhau được cung cấp, nó vượt qua các bộ thử nghiệm tính ngẫu nhiên hiện tại với màu sắc bay tốc độ cực nhanh, điều đó khiến bạn tự hỏi tại sao nó chưa được đưa vào thư viện chuẩn.


3
Đó chính xác là sự kết hợp của srandvà một thuật toán không xác định std::rand gặp rắc rối. Xem thêm câu trả lời của tôi cho một câu hỏi khác .
Peter O.

2
randvề cơ bản bị giới hạn ở cấp API trong đó hạt giống (và do đó số lượng trình tự có thể được tạo ra) bị giới hạn bởi UINT_MAX+1.
R .. GitHub DỪNG TRỢ GIÚP LÚC NỮA,

2
chỉ một lưu ý: minstd là một PRNG tồi, mt19973 tốt hơn nhưng không nhiều: pcg-random.org/… (trong biểu đồ đó, minstd == LCG32 / 64). Thật đáng tiếc khi C ++ không cung cấp bất kỳ PRNG chất lượng cao, nhanh chóng như PCG hoặc xoroshiro128 +.
user60561

2
@MatteoItalia Chúng tôi không bất đồng. Đây cũng là quan điểm của Bjarne. Chúng tôi thực sự muốn có <random>trong tiêu chuẩn, nhưng chúng tôi cũng muốn có tùy chọn "chỉ cần cung cấp cho tôi một bản triển khai tốt mà tôi có thể sử dụng ngay bây giờ". Đối với PRNG cũng như những thứ khác.
ravnsgaard

2
Một vài lưu ý: 1. Thay thế std::uniform_int_distribution<int> dist{0, 5}(eng);bằng eng() % 6giới thiệu lại hệ số lệch mà std::randmã phải chịu (thừa nhận là sai lệch nhỏ trong trường hợp này, nơi động cơ có 2**31 - 1đầu ra và bạn đang phân phối chúng cho 6 nhóm). 2. Trên ghi chú của bạn về " srandmất một unsigned int" giới hạn các kết quả đầu ra có thể, như đã viết, việc gieo hạt cho động cơ của bạn chỉ std::random_device{}()có cùng một vấn đề; bạn cần một seed_seqđể khởi tạo đúng hầu hết các PRNG .
ShadowRanger

6

Nếu bạn lặp lại thử nghiệm của mình với phạm vi lớn hơn 5 thì bạn có thể sẽ thấy các kết quả khác nhau. Khi phạm vi của bạn nhỏ hơn đáng kể RAND_MAXthì không có vấn đề gì đối với hầu hết các ứng dụng.

Ví dụ: nếu chúng ta có RAND_MAX25 thì rand() % 5sẽ tạo ra các số có tần số sau:

0: 6
1: 5
2: 5
3: 5
4: 5

Như RAND_MAXđược đảm bảo là lớn hơn 32767 và sự khác biệt về tần số giữa ít khả năng nhất và nhiều khả năng nhất chỉ là 1, đối với các số nhỏ, phân phối gần đủ ngẫu nhiên cho hầu hết các trường hợp sử dụng.


3
Này được giải thích trượt thứ hai STL của
Alan Birtles

4
Ok, nhưng ... STL là ai? Và những slide nào? (câu hỏi nghiêm túc)
kebs

@kebs, Stephan Lavavej, hãy xem tài liệu tham khảo trên Youtube trong câu hỏi.
Evg

3

Đầu tiên, đáng ngạc nhiên là câu trả lời thay đổi tùy thuộc vào việc bạn đang sử dụng số ngẫu nhiên để làm gì. Nếu nó là để lái xe, chẳng hạn, một trình thay đổi màu nền ngẫu nhiên, sử dụng rand () là hoàn toàn tốt. Nếu bạn đang sử dụng một số ngẫu nhiên để tạo một ván bài poker ngẫu nhiên hoặc một khóa an toàn bằng mật mã thì điều đó không ổn.

Khả năng dự đoán: dãy số 012345012345012345012345 ... sẽ cung cấp phân phối đồng đều của mỗi số trong mẫu của bạn, nhưng rõ ràng không phải là ngẫu nhiên. Đối với một dãy là ngẫu nhiên, không thể dễ dàng dự đoán giá trị của n + 1 bằng giá trị của n (hoặc thậm chí bởi các giá trị của n, n-1, n-2, n-3, v.v.) Rõ ràng là một dãy lặp của các chữ số giống nhau là trường hợp suy biến, nhưng một chuỗi được tạo bằng bất kỳ bộ tạo đồng dư tuyến tính nào đều có thể được phân tích; nếu bạn sử dụng các cài đặt ngoài hộp mặc định của một LCG thông thường từ một thư viện chung, kẻ độc hại có thể "phá vỡ trình tự" mà không cần nỗ lực nhiều. Trước đây, một số sòng bạc trực tuyến (và một số sòng bạc truyền thống) đã bị thua lỗ bởi các máy sử dụng bộ tạo số ngẫu nhiên kém. Ngay cả những người đáng lẽ phải biết rõ hơn cũng đã bị bắt kịp;

Phân phối: Như được đề cập trong video, lấy mô-đun 100 (hoặc bất kỳ giá trị nào không chia đều cho độ dài của chuỗi) sẽ đảm bảo rằng một số kết quả sẽ ít nhất có khả năng cao hơn một chút so với các kết quả khác. Trong vũ trụ của 32767 giá trị bắt đầu có thể có theo modulo 100, các số từ 0 đến 66 sẽ xuất hiện 328/327 (0,3%) thường xuyên hơn các giá trị 67 đến 99; một yếu tố có thể mang lại lợi thế cho kẻ tấn công.


1
"Khả năng dự đoán: dãy số 012345012345012345012345 ... sẽ vượt qua bài kiểm tra của bạn về" tính ngẫu nhiên ", trong đó sẽ có phân phối đồng đều của mỗi số trong mẫu của bạn" thực sự không phải vậy; những gì anh ta đang đo là stddev của stddev giữa các lần chạy, tức là về cơ bản cách biểu đồ các lần chạy khác nhau được trải ra. Với máy phát điện 012345012345012345 ... nó sẽ luôn là con số không.
Matteo Italia

Điểm tốt; Tôi đọc qua mã của OP hơi nhanh quá, tôi sợ. Đã chỉnh sửa câu trả lời của tôi để phản ánh.
JackLThornton

Hehe Tôi biết vì tôi mặc dù để làm bài kiểm tra đó là tốt, và tôi nhận thấy tôi thu được kết quả khác nhau 😄
Matteo Italia

1

Câu trả lời đúng là: nó phụ thuộc vào ý bạn muốn nói "tốt hơn".

Các <random>engine "mới" đã được giới thiệu với C ++ hơn 13 năm trước, vì vậy chúng không thực sự mới. Thư viện C rand()đã được giới thiệu cách đây nhiều thập kỷ và đã rất hữu ích trong thời gian đó cho bất kỳ thứ gì.

Thư viện tiêu chuẩn C ++ cung cấp ba lớp của công cụ tạo số ngẫu nhiên: Thông số tuyến tính (trong đó rand()là một ví dụ), Fibonacci trễ và Mersenne Twister. Mỗi lớp đều có sự cân bằng và mỗi lớp là "tốt nhất" theo những cách nhất định. Ví dụ, các LCG có trạng thái rất nhỏ và nếu các thông số phù hợp được chọn, khá nhanh trên các bộ xử lý máy tính để bàn hiện đại. Các LFG có trạng thái lớn hơn và chỉ sử dụng tìm nạp bộ nhớ và hoạt động bổ sung, do đó, rất nhanh trên các hệ thống nhúng và vi điều khiển thiếu phần cứng toán học chuyên biệt. MTG có trạng thái rất lớn và chậm, nhưng có thể có một chuỗi không lặp lại rất lớn với các đặc điểm phổ tuyệt vời.

Nếu không có trình tạo nào được cung cấp đủ tốt cho mục đích sử dụng cụ thể của bạn, thư viện chuẩn C ++ cũng cung cấp giao diện cho trình tạo phần cứng hoặc công cụ tùy chỉnh của riêng bạn. Không có trình tạo nào được thiết kế để sử dụng độc lập: mục đích sử dụng của chúng là thông qua đối tượng phân phối cung cấp một chuỗi ngẫu nhiên với một hàm phân phối xác suất cụ thể.

Một ưu điểm khác của <random>over rand()rand()sử dụng trạng thái toàn cục, không được đăng nhập lại hoặc luồng an toàn và cho phép một phiên bản duy nhất cho mỗi quy trình. Nếu bạn cần kiểm soát chi tiết hoặc khả năng dự đoán (tức là có thể tạo ra một lỗi ở trạng thái hạt giống RNG) thì điều đó rand()là vô ích. Các <random>máy phát điện được instanced tại địa phương và có serializable (và restorable) nhà nước.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.