Цей приклад коду ілюструє, що std::randце випадок застарілого вантажного культового балдердашу, який повинен змушувати піднімати брови кожного разу, коли ви його бачите.
Тут є кілька питань:
Люди, які підписують контракт, зазвичай припускають - навіть бідні нещасні душі, які не знають нічого кращого і не думають про це саме в цих термінах, - це randвибірки з рівномірного розподілу на цілі числа в 0, 1, 2,… RAND_MAX,, і кожен виклик дає незалежний зразок.
Перша проблема полягає в тому, що передбачуваний контракт, незалежні єдині випадкові вибірки в кожному виклику, насправді не є тим, що йдеться в документації, і на практиці реалізація історично не могла забезпечити навіть найпростіший симулякр незалежності. Наприклад, C99 § 7.20.2.1 " randФункція" говорить без деталізації:
randФункція обчислює послідовність псевдовипадкових чисел в діапазоні від 0 до RAND_MAX.
Це безглузде речення, оскільки псевдовипадковість - це властивість функції (або сімейства функцій ), а не цілого числа, але це не заважає навіть чиновникам ISO не зловживати мовою. Зрештою, єдині читачі, які б це засмутили, знають краще, ніж читати документацію, randбо бояться, що їх мозкові клітини розпадуться.
Типова історична реалізація на C працює так:
static unsigned int seed = 1;
static void
srand(unsigned int s)
{
seed = s;
}
static unsigned int
rand(void)
{
seed = (seed*1103515245 + 12345) % ((unsigned long)RAND_MAX + 1);
return (int)seed;
}
Це має прикрою властивістю те, що, хоча один зразок може бути рівномірно розподілений під рівномірним випадковим насінням (що залежить від конкретного значення RAND_MAX), він чергує між парними та непарними цілими числами в послідовних викликах - після
int a = rand();
int b = rand();
вираз (a & 1) ^ (b & 1)дає 1 зі 100% -ною ймовірністю, що не стосується незалежних випадкових вибірок у будь-якому розподілі, підтримуваному на парні та непарні числа. Таким чином, виник культовий культ, що слід відкидати біти низького порядку, щоб переслідувати невловимого звіра на «кращу випадковість». (Попередження спойлера: Це не технічний термін. Це ознака того, що чиюсь прозу ви читаєте, чи не знає, про що вони говорять, або вважає, що ви незрозумілі і до неї потрібно поблажливо.)
Друга проблема полягає в тому, що навіть якби кожен виклик робив вибірку незалежно від рівномірного випадкового розподілу на 0, 1, 2, ..., RAND_MAXрезультат rand() % 6не був би розподілений рівномірно в 0, 1, 2, 3, 4, 5, як штамп рол, якщо RAND_MAXце не відповідає -1 модулю 6. Простий контрприклад: Якщо RAND_MAX= 6, то з rand(), всі результати мають рівну ймовірність 1/7, але з rand() % 6, результат 0 має ймовірність 2/7, тоді як усі інші результати мають ймовірність 1/7 .
Правильний спосіб зробити це за допомогою вибірки відхилення: кілька разів малюйте незалежну рівномірну випадкову вибірку sз 0, 1, 2,… RAND_MAX, і відхиляйте (наприклад) результати 0, 1, 2,…, ((RAND_MAX + 1) % 6) - 1- якщо ви отримаєте один із ті, почніть спочатку; інакше врожайність s % 6.
unsigned int s;
while ((s = rand()) < ((unsigned long)RAND_MAX + 1) % 6)
continue;
return s % 6;
Таким чином, набір результатів, rand()які ми приймаємо, рівномірно ділиться на 6, і кожен можливий результат від s % 6цього отримується однаковою кількістю прийнятих результатів rand(), тому якщо rand()рівномірно розподілений, то так і є s. Кількість випробувань не обмежена , але очікувана кількість менше 2, а ймовірність успіху зростає в експоненціальній залежності від кількості випробувань.
Вибір яких Результати rand()відхиленим несуттєво, при умови , що ви на карту рівного числа їх з кожним цілим числом нижче 6. код на cppreference.com робить інший вибір, з - за першу проблему вище , що нічого не гарантовано про розподіл або незалежність виходів rand(), і на практиці біти низького порядку демонстрували шаблони, які не виглядають "досить випадковими" (не майте на увазі, що наступний вихід є детермінованою функцією попереднього).
Вправа для читача: Доведіть , що код на cppreference.com дає рівномірний розподіл на штампованих рулонах , якщо rand()дає рівномірний розподіл на 0, 1, 2, ..., RAND_MAX.
Вправа для читача: Чому ви можете віддати перевагу одній або іншій підмножині? Яке обчислення потрібно для кожного випробування у двох випадках?
Третя проблема полягає в тому, що насіннєвий простір настільки малий, що навіть якщо насіння є рівномірно розподіленим, супротивник, озброєний знаннями вашої програми та одним результатом, але не насінням, може легко передбачити насіння та подальші результати, через що вони здаються не такими випадковий зрештою. Тому навіть не думайте використовувати це для криптографії.
Ви можете піти на химерний переобладнаний маршрут та std::uniform_int_distributionклас C ++ 11 з відповідним випадковим пристроєм та вашим улюбленим випадковим двигуном, як колись популярний твістер Mersenne, std::mt19937щоб грати у кубики зі своїм чотирирічним двоюрідним братом, але навіть це не збирається бути придатним для створення криптографічного ключового матеріалу - і твістер Mersenne - це жахливий космічний гугл із багатокілобайтним станом, що сприймає загрозу в кеші вашого процесора з нецензурним часом настройки, тому це погано навіть для, наприклад , паралельних моделювань Монте-Карло з відтворювані дерева підрахунків; її популярність, швидше за все, виникає в основному від його привабливої назви. Але ви можете використовувати його для прокатки іграшкових кісток, як цей приклад!
Інший підхід полягає у використанні простого генератора криптографічних псевдовипадкових чисел з невеликим станом, наприклад простого швидкого стирання клавіш PRNG , або просто потокового шифру, такого як AES-CTR або ChaCha20, якщо ви впевнені ( наприклад , у моделюванні в Монте-Карло для дослідження в природничих науках) про те, що передбачення минулих результатів не матиме несприятливих наслідків, якщо держава буде коли-небудь порушена.
std::uniform_int_distributionдля кубиків