Чому нова випадкова бібліотека краща за std :: rand ()?


82

Тож я побачив виступ під назвою rand (), який вважається шкідливим, і він виступав за використання парадигми розподілу двигуна генерації випадкових чисел над простою std::rand()парадигмою плюс модуль.

Однак я хотів побачити недоліки з std::rand()перших вуст, тому провів швидкий експеримент:

  1. В принципі, я написав 2 функції getRandNum_Old()і , getRandNum_New()що генерується випадкове число в діапазоні від 0 до 5 включно , використовуючи std::rand()і std::mt19937+ std::uniform_int_distributionвідповідно.
  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()+ %. Іншим зауваженням, яке я зробив, було те, що нове було приблизно в чотири рази повільніше, ніж колишнє. Загалом, здавалося, що я плачу величезні витрати на швидкість майже за відсутність виграшу в якості.

Мій експеримент якимось чином помилковий? Або 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));
}

32
Ось чому ця порада існує. Якщо ви не знаєте, як перевірити RNG на достатню ентропію, чи це важливо для вашої програми, тоді слід вважати, що std :: rand () недостатньо хороший. en.wikipedia.org/wiki/Entropy_(computing)
Ганс Пассант,

4
Суть у тому, чи rand()достатньо добре, багато в чому залежить від того, для чого ви використовуєте колекцію випадкових чисел. Вам потрібен певний тип випадкового розподілу, тоді, звичайно, реалізація бібліотеки буде кращою. Якщо вам просто потрібні випадкові числа і вас не хвилює "випадковість" або тип розподілу, тоді rand()це нормально. Підберіть відповідний інструмент до вашої роботи.
Девід К. Ранкін,

2
можлива дурень: stackoverflow.com/questions/52869166/... Я просто не хочу забивати цю, тому я утримуюся від фактичного голосування.
болов

18
for (i=0; i<k*n; i++) a[i]=i%n;виробляє таке саме середнє та середньоквадратичне відхилення, як і найкращий СПГ там. Якщо це достатньо для вашої програми, просто використовуйте цю послідовність.
п. 'займенники' m.

3
"середньоквадратичне відхилення якомога менше" - ні. Це неправильно. Ви очікуєте, що частоти будуть дещо іншими - приблизно sqrt (частота) - це приблизно те, що, як ви очікуєте, буде стандартним відхиленням. "Лічильник збільшення", який виробляється нм, матиме набагато нижчий коефіцієнт сд (і це дуже поганий коефіцієнт корисної дії).
Мартін Боннер підтримує Моніку

Відповіді:


106

Практично будь-яка реалізація "старих" rand()використовує LCG ; хоча вони, як правило, не найкращі генератори навколо, як правило, ви не побачите, як вони виходять з ладу на такому базовому тесті - середнє та стандартне відхилення, як правило, виправляється навіть у найгірших PRNG.

Поширені помилки "поганих", але досить загальних rand()реалізацій:

  • низька випадковість бітів нижчого порядку;
  • короткий період;
  • низький RAND_MAX;
  • певна кореляція між послідовними вилученнями (загалом, LCG виробляють числа, які знаходяться на обмеженій кількості гіперпланів, хоча це можна якось пом'якшити).

Проте, жодне з них не є специфічним для API rand(). Конкретна реалізація може розмістити генератор сімейства xorshift позаду srand/ randі, алгоритмічно кажучи, отримати сучасний PRNG без змін інтерфейсу, тому жоден тест, подібний тому, який ви зробили, не виявив би слабкості у результатах.

Редагувати: @R. правильно зазначає, що інтерфейс rand/ srandобмежений тим фактом, який srandприймає unsigned int, тому будь-який генератор, який реалізація може поставити за собою, по суті обмежений UINT_MAXможливими початковими насінням (і, таким чином, генерованими послідовностями). Це дійсно так, хоча API може бути тривіально розширено , щоб srandприйняти unsigned long long, або додати окрему srand(unsigned char *, size_t)перевантаження.


Дійсно, фактична проблема 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 для нас, некомплікативних людей, які хотіли б не тонути в випадкових пристроях / двигунах / адаптерах / незалежно від того, що ми хочемо витягнути номер для карток бінго), але це правда побудуйте його самостійно на поточних потужностях (хоча створення "повного" 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*, і це в 2,3 рази швидше, ніж rand()(3,68 мс проти 8,61 мс); враховуючи, що, на відміну від Mersenne Twister та різних наданих LCG, він проходить поточні набори тестів на випадковість із яскравими кольорами, і це надзвичайно швидко, це змушує задуматися, чому це ще не включено до стандартної бібліотеки.


3
Саме проблема поєднання srandта неуточненого алгоритму потрапляє std::randв біду. Дивіться також мою відповідь на інше запитання .
Пітер О.

2
randпринципово обмежений на рівні API, оскільки насіння (і, отже, кількість можливих послідовностей, які можуть бути створені) обмежується UINT_MAX+1.
R .. GitHub СТОП ДОПОМОГАЙ ЛЕД

2
лише примітка: minstd - це поганий PRNG, mt19973 краще, але не набагато: pcg-random.org/… (у цій таблиці minstd == LCG32 / 64). Шкода, що C ++ не забезпечує якісних, швидких PRNG, таких як PCG або xoroshiro128 +.
user60561

2
@MatteoItalia Ми не розходимось. Це також було питання Бьярна. Ми дуже хочемо <random>стандарту, але ми також хотіли б варіант "просто дайте мені гідну реалізацію, яку я можу зараз використовувати". Як для PRNG, так і для інших речей.
ravnsgaard

2
Пара зауважень: 1. Заміна std::uniform_int_distribution<int> dist{0, 5}(eng);на eng() % 6повторно вводить коефіцієнт перекосу, від якого std::randстраждає код (правда, незначний перекіс у цьому випадку, коли двигун має 2**31 - 1вихідні дані, а ви розподіляєте їх на 6 сегментів). 2. На Вашій записці про " srandприймає unsigned int", яке обмежує можливі результати, як написано, засівання вашого двигуна має саме std::random_device{}()ту ж проблему; вам потрібна, seed_seqщоб правильно ініціалізувати більшість PRNG .
ShadowRanger

6

Якщо ви повторите експеримент із діапазоном більше 5, то, ймовірно, ви побачите різні результати. Коли ваш діапазон значно менший ніжRAND_MAX для більшості програм, проблема не виникає.

Наприклад, якщо ми маємо RAND_MAX25, тоді rand() % 5буде видаватися числа з такими частотами:

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

Оскільки RAND_MAXгарантовано більше 32767, а різниця в частотах між найменш ймовірною та найімовірнішою становить лише 1, для малих чисел розподіл є майже досить випадковим для більшості випадків використання.


3
Це пояснюється в другому слайді STL
Алан Біртлз,

4
Гаразд, але ... хто такий STL? А які слайди? (серйозне запитання)
kebs

@kebs, Стефан Лававей, див. посилання на Youtube у питанні.
Evg

3

По-перше, як не дивно, відповідь змінюється залежно від того, для чого ви використовуєте випадкове число. Якщо це керування, скажімо, випадковим перемикачем кольорів тла, використання rand () цілком нормально. Якщо ви використовуєте випадкове число для створення випадкової руки в покер або криптографічно захищеного ключа, то це не добре.

Передбачуваність: послідовність 012345012345012345012345 ... забезпечить рівномірний розподіл кожного числа у вашій вибірці, але, очевидно, не є випадковою. Щоб послідовність була випадковою, значення n + 1 неможливо легко передбачити за значенням n (або навіть за значеннями n, n-1, n-2, n-3 тощо) Очевидно, що повторювана послідовність з тих самих цифр - вироджений випадок, але послідовність, генерована за допомогою будь-якого лінійного конгруентного генератора, може бути піддана аналізу; якщо ви використовуєте стандартні налаштування загальної LCG із загальної бібліотеки, зловмисна особа може "порушити послідовність" без особливих зусиль. У минулому кілька онлайнових казино (і деякі цегляно-будівельні) зазнали збитків від машин, які використовували неякісні генератори випадкових чисел. Навіть людей, які мали б краще знати, наздогнали;

Розподіл: Як зазначається у відео, прийняття модуля 100 (або будь-якого значення, яке не рівномірно ділиться на довжину послідовності) гарантуватиме, що деякі результати стануть принаймні трохи більш імовірними, ніж інші результати. У всесвіті 32767 можливих початкових значень за модулем 100 числа від 0 до 66 будуть частіше відображатися на 328/327 (0,3%), ніж значення від 67 до 99; фактор, який може надати зловмисникові перевагу.


1
"Передбачуваність: послідовність 012345012345012345012345 ... пройде ваш тест на" випадковість ", оскільки буде рівномірний розподіл кожного числа у вашій вибірці" насправді, а не насправді; те, що він вимірює, це stddev stddevs між прогонами, тобто, по суті, як розподіляються гістограми різних прогонів. З генератором 012345012345012345 ... це завжди було б нулем.
Matteo Italia

Влучне зауваження; Боюсь, я прочитав код OP занадто швидко. Редагував мою відповідь для роздумів.
JackLThornton

Хе-хе, я знаю, бо я хотів би зробити і цей тест, і я помітив, що отримав різні результати 😄
Matteo Italia

1

Правильна відповідь така: це залежить від того, що ви маєте на увазі під словом «краще».

"Нові" <random>двигуни були представлені в C ++ понад 13 років тому, тому вони насправді не нові. Бібліотека Crand() була введена десятки років тому і була дуже корисною в той час для будь-якої кількості речей.

Стандартна бібліотека C ++ забезпечує три класи механізмів генератора випадкових чисел: лінійний конгруентний (з них rand() є прикладом), Lagged Fibonacci та Mersenne Twister. Існують компроміси кожного класу, і кожен клас є "найкращим" певними способами. Наприклад, LCG мають дуже малий стан, і якщо обрані правильні параметри, досить швидко на сучасних настільних процесорах. LFG мають більший стан і використовують лише отримання пам’яті та операції додавання, тому дуже швидко працюють на вбудованих системах та мікроконтролерах, у яких відсутнє спеціалізоване математичне обладнання. MTG має величезний стан і повільний, але може мати дуже велику неповторювану послідовність з чудовими спектральними характеристиками.

Якщо жоден із генераторів, що входять до комплекту, не підходить для вашого конкретного використання, стандартна бібліотека C ++ також надає інтерфейс або для апаратного генератора, або для вашого власного механізму. Жоден з генераторів не призначений для використання в автономному режимі: їх передбачуване використання здійснюється через об'єкт розподілу, який забезпечує випадкову послідовність з певною функцією розподілу ймовірностей.

Ще однією перевагою <random>над rand()є теrand() використовує глобальний стан, не перенаправляється або не захищає потоки, а також дозволяє один екземпляр на процес. Якщо вам потрібен чіткий контроль або передбачуваність (тобто здатний відтворити помилку, враховуючи стан насіння RNG), тоді rand()марно. В <random>генераторах локально інстанси і мають Серіалізуемое (і відновлюване) стан.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.