Цей приклад коду ілюструє, що 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
для кубиків