Этот пример кода демонстрирует, что 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 с подходящим случайным устройством и вашим любимым случайным движком, таким как вечно популярный твистер Мерсенна, std::mt19937
чтобы играть в кости со своим четырехлетним кузеном, но даже это не поможет быть пригодным для генерации материала криптографического ключа - и Твистер Мерсенна тоже ужасный мусорщик с многокилобайтным состоянием, наносящим ущерб кеш-памяти вашего процессора с неприличным временем настройки, так что это плохо даже для, например , параллельного моделирования Монте-Карло с воспроизводимые деревья подвычислений; его популярность, вероятно, обусловлена броским названием. Но вы можете использовать его для катания игрушечных кубиков, как в этом примере!
Другой подход заключается в использовании простого криптографического генератора псевдослучайных чисел с небольшим состоянием, такого как простой ГПСЧ быстрого стирания ключа , или просто потокового шифра, такого как AES-CTR или ChaCha20, если вы уверены ( например , в моделировании Монте-Карло для исследования в области естественных наук), что нет никаких неблагоприятных последствий для прогнозирования прошлых результатов, если состояние когда-либо будет скомпрометировано.
std::uniform_int_distribution
для игры в кости