Почему rand () повторяет числа гораздо чаще в Linux, чем в Mac?

88

Я реализовывал хэш-карту в C как часть проекта, над которым я работаю, и использовал случайные вставки для его проверки, когда заметил, что rand()в Linux кажется, что цифры повторяются гораздо чаще, чем в Mac. RAND_MAXравно 2147483647 / 0x7FFFFFFF на обеих платформах. Я сократил его до этой тестовой программы, которая создает массив байтов RAND_MAX+1длиной, генерирует RAND_MAXслучайные числа, отмечает, если каждое из них является дубликатом, и проверяет его из списка, как видно.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

int main() {
    size_t size = ((size_t)RAND_MAX) + 1;
    char *randoms = calloc(size, sizeof(char));
    int dups = 0;
    srand(time(0));
    for (int i = 0; i < RAND_MAX; i++) {
        int r = rand();
        if (randoms[r]) {
            // printf("duplicate at %d\n", r);
            dups++;
        }
        randoms[r] = 1;
    }
    printf("duplicates: %d\n", dups);
}

Linux последовательно генерирует около 790 миллионов дубликатов. Mac последовательно генерирует только одно, поэтому он просматривает каждое случайное число, которое может генерировать почти без повторения. Может кто-нибудь объяснить мне, как это работает? Я не могу сказать ничего отличного от man-страниц, не могу сказать, какой RNG использует каждый, и не могу найти что-либо в Интернете. Спасибо!

Терон С
источник
4
Поскольку rand () возвращает значения от 0..RAND_MAX включительно, размер вашего массива должен быть RAND_MAX + 1
Blastfurnace
21
Вы могли заметить, что RAND_MAX / e ~ = 790 миллионов. Также предел (1-1 / n) ^ n при приближении n к бесконечности равен 1 / e.
Дэвид Шварц
3
@DavidSchwartz Если я правильно вас понимаю, это может объяснить, почему число в Linux постоянно составляет около 790 миллионов. Наверное, тогда возникает вопрос: почему / как Mac не повторяется так много раз?
Терон С
26
В библиотеке времени выполнения нет требований к качеству PRNG. Единственное реальное требование - повторяемость с тем же семенем. Очевидно, что качество PRNG в вашем Linux лучше, чем в вашем Mac.
pmg
4
@chux Да, но так как он основан на умножении, состояние никогда не может быть нулевым, или результат (следующее состояние) также будет нулевым. Основываясь на исходном коде, он проверяет ноль в качестве особого случая, если он заполнен нулями, но он никогда не выдает ноль как часть последовательности.
Арку

Ответы:

119

Хотя на первый взгляд может показаться, что macOS rand()лучше не повторять никаких чисел, следует отметить, что при таком количестве генерируемых чисел ожидается множество дубликатов (на самом деле около 790 миллионов или (2 31 -1). ) / е ). Аналогичным образом, повторение чисел в последовательности также не приведет к дублированию, но не будет считаться очень случайным. Таким образом, rand()реализация Linux в этом тесте неотличима от истинного случайного источника, тогда как macOS rand()- нет.

Еще одна вещь, которая на первый взгляд кажется удивительной, - это то, как macOS rand()может так хорошо избегать дублирования. Глядя на его исходный код , мы обнаруживаем, что реализация выглядит следующим образом:

/*
 * Compute x = (7^5 * x) mod (2^31 - 1)
 * without overflowing 31 bits:
 *      (2^31 - 1) = 127773 * (7^5) + 2836
 * From "Random number generators: good ones are hard to find",
 * Park and Miller, Communications of the ACM, vol. 31, no. 10,
 * October 1988, p. 1195.
 */
    long hi, lo, x;

    /* Can't be initialized with 0, so use another value. */
    if (*ctx == 0)
        *ctx = 123459876;
    hi = *ctx / 127773;
    lo = *ctx % 127773;
    x = 16807 * lo - 2836 * hi;
    if (x < 0)
        x += 0x7fffffff;
    return ((*ctx = x) % ((unsigned long) RAND_MAX + 1));

Это действительно приводит ко всем числам от 1 до RAND_MAX, включительно, ровно один раз, прежде чем последовательность повторяется снова. Поскольку следующее состояние основано на умножении, состояние никогда не может быть нулевым (или все будущие состояния также будут равны нулю). Таким образом, повторное число, которое вы видите, является первым, а ноль - тем, которое никогда не возвращается.

Apple продвигает использование лучших генераторов случайных чисел в своей документации и примерах, по крайней мере, до тех пор, пока существует macOS (или OS X), поэтому качество, rand()вероятно, не считается важным, и они просто придерживались одного из самые простые из доступных псевдослучайных генераторов. (Как вы заметили, их rand()даже комментируют с рекомендацией использовать arc4random()вместо этого.)

На заметку о том, что самый простой генератор псевдослучайных чисел, который я нашел, который дает достойные результаты в этом (и многих других) тестах на случайность, это xorshift * :

uint64_t x = *ctx;
x ^= x >> 12;
x ^= x << 25;
x ^= x >> 27;
*ctx = x;
return (x * 0x2545F4914F6CDD1DUL) >> 33;

Результатом этой реализации будет почти ровно 790 миллионов дубликатов в вашем тесте.

Arkku
источник
5
В журнальной статье, опубликованной в 1980-х годах, был предложен статистический тест для PRNG, основанный на «проблеме дня рождения».
PJs
14
«Apple продвигает использование лучших генераторов случайных чисел в своей документации» -> конечно, Apple может использовать arc4random()подобный код rand()и получить хороший rand()результат. Вместо того чтобы пытаться заставить программистов по-другому писать код, просто создайте лучшие библиотечные функции. «Они только что застряли» - это их выбор.
chux - Восстановить Монику
23
Отсутствие постоянного смещения в Mac rand()делает его настолько плохим, что это бесполезно для практического использования: почему rand ()% 7 всегда возвращает 0? , Rand ()% 14 генерирует только значения 6 или 13
phuclv
4
@PeterCordes: существует такое требование rand, чтобы при повторном запуске с тем же начальным числом создавалась та же последовательность. OpenBSD не randработает и не подчиняется этому контракту.
R .. GitHub ОСТАНОВИТЬСЯ ПОМОЩЬ ЛЬДУ
8
@ R..GitHubSTOPHELPINGICE Видите ли вы требование C, чтобы rand()с одним и тем же начальным числом создавалась одинаковая последовательность между разными версиями библиотеки? Такая гарантия может быть полезна для регрессионного тестирования между версиями библиотеки, но я не нахожу требований к Си для этого.
chux - Восстановить Монику
34

MacOS предоставляет недокументированную функцию rand () в stdlib. Если оставить его незаполненным, то первые значения, которые он выводит, это 16807, 282475249, 1622650073, 984943658 и 1144108930. Быстрый поиск покажет, что эта последовательность соответствует основному генератору случайных чисел LCG, который выполняет следующую формулу:

x n +1 = 7 5 · x n (мод 2 31 - 1)

Поскольку состояние этого RNG полностью описывается значением одного 32-разрядного целого числа, его период не очень велик. Чтобы быть точным, он повторяется каждые 2 31 - 2 итерации, выводя каждое значение от 1 до 2 31 - 2.

Я не думаю, что есть стандартная реализация rand () для всех версий Linux, но есть функция glibc rand (), которая часто используется. Вместо одной 32-битной переменной состояния, в ней используется пул из более чем 1000 битов, который, по сути, никогда не будет создавать полностью повторяющуюся последовательность. Опять же, вы, вероятно, можете узнать, какая у вас версия, распечатав первые несколько выводов с этого ГСЧ, не заполнив его сначала. (Функция glibc rand () создает числа 1804289383, 846930886, 1681692777, 1714636915 и 1957747793.)

Поэтому причина того, что вы получаете больше коллизий в Linux (и вряд ли в MacOS), заключается в том, что версия rand () для Linux в основном более случайная.

r3mainer
источник
5
не отобранный rand()должен вести себя как один сsrand(1);
pmg
5
rand()Доступен исходный код для MacOS: opensource.apple.com/source/Libc/Libc-1353.11.2/stdlib/FreeBSD/… FWIW, я выполнил тот же тест для этого, скомпилированного из исходного кода, и он действительно приводит к только один дубликат. Apple продвигает использование других генераторов случайных чисел (например, arc4random()до того, как Swift вступил во владение) в их примерах и документации, поэтому, rand()вероятно, использование не очень распространено в нативных приложениях на их платформах, что может объяснить, почему это не лучше.
Арку
Спасибо за ответ, который отвечает на мой вопрос. И период (2 ^ 31) -2 объясняет, почему это начало повторяться прямо в конце, как я наблюдал. Вы (@ r3mainer) сказали rand(), что у вас нет документов, но @Arkku предоставила ссылку на очевидный источник. Кто-нибудь из вас знает, почему я не могу найти этот файл в моей системе и почему я вижу только int rand(void) __swift_unavailable("Use arc4random instead.");в Mac stdlib.h? Я предполагаю, что код @Arkku, на который ссылается, только что скомпилирован в ... какую библиотеку?
Терон С
1
@TheronS Она составлена в библиотеку C, LIBC, /usr/lib/libc.dylib. =)
Арку
5
Какая версия rand()данной программы , использования C не определяется «компилятор» или «операционной системы», а скорее реализации стандартной библиотеки С (например, glibc, libc.dylib, msvcrt*.dll).
Питер О.
10

rand()определяется стандартом C, а стандарт C не определяет, какой алгоритм использовать. Очевидно, что Apple использует подчиненный алгоритм для вашей реализации GNU / Linux: Linux не отличается от истинного случайного источника в вашем тесте, в то время как реализация Apple просто перемешивает числа вокруг.

Если вам нужны случайные числа любого качества, либо используйте более качественный PRNG, который дает хотя бы некоторые гарантии качества возвращаемых чисел, либо просто считываете с него /dev/urandomили тому подобное. Последний дает вам криптографическое качество, но это медленно. Даже если он сам по себе слишком медленный, он /dev/urandomможет обеспечить отличные семена для других, более быстрых PRNG.

cmaster - восстановить монику
источник
Спасибо за ответ. На самом деле мне не нужен хороший PRNG, я просто был обеспокоен тем, что в моей хэш-карте скрывалось какое-то неопределенное поведение, затем мне стало любопытно, когда я исключил эту возможность, и платформы по-прежнему вели себя по-разному.
Терон С
Кстати, вот пример криптографически безопасного генератора случайных чисел: github.com/divinity76/phpcpp/commit/… - но это C ++ вместо C, и я позволяю разработчикам STL делать всю тяжелую работу ...
hanshenrik
3
@hanshenrik Криптографический RNG обычно избыточен и слишком медленен для простой хеш-таблицы.
PM 2Ring
1
@ PM2Ring Абсолютно. Хеш-таблица должна быть быстрой, а не хорошей. Однако, если вы хотите разработать алгоритм хеш-таблицы, который будет не только быстрым, но и приличным, я считаю, что полезно знать некоторые приемы криптографических хеш-алгоритмов. Это поможет вам избежать большинства самых вопиющих ошибок, которые загадывают самые быстрые алгоритмы хэширования. Тем не менее, я бы не рекламировал здесь конкретную реализацию.
cmaster - восстановить монику
@cmaster Достаточно верно. Конечно, неплохо бы немного узнать о таких вещах, как функции микширования и лавинообразный эффект . К счастью, существуют не криптографические хэш-функции с хорошими свойствами, которые не жертвуют слишком большой скоростью (если они реализованы правильно), например, xxhash, murmur3 или siphash.
PM 2Ring
5

В общем, пара rand / srand долгое время считалась устаревшей из-за того, что биты младшего разряда отображают меньшую случайность, чем биты старшего разряда в результатах. Это может или не может иметь какое-либо отношение к вашим результатам, но я думаю, что это все еще хорошая возможность помнить, что, хотя некоторые реализации rand / srand теперь более современны, более старые реализации сохраняются, и лучше использовать случайные (3 ). На моем компьютере Arch Linux следующее примечание все еще находится на странице руководства для rand (3):

  The versions of rand() and srand() in the Linux C Library use the  same
   random number generator as random(3) and srandom(3), so the lower-order
   bits should be as random as the higher-order bits.  However,  on  older
   rand()  implementations,  and  on  current implementations on different
   systems, the lower-order bits are much less random than the  higher-or-
   der bits.  Do not use this function in applications intended to be por-
   table when good randomness is needed.  (Use random(3) instead.)

Чуть ниже справочная страница на самом деле дает очень короткие, очень простые примеры реализации rand и srand, которые относятся к самым простым LC RNG, которые вы когда-либо видели, и имеют небольшой RAND_MAX. Я не думаю, что они соответствуют тому, что находится в стандартной библиотеке C, если они когда-либо делали. Или, по крайней мере, я надеюсь, что нет.

В общем, если вы собираетесь использовать что-то из стандартной библиотеки, используйте случайное, если можете (на странице руководства он перечисляется как стандарт POSIX до POSIX.1-2001, но rand является стандартным способом еще до того, как C был даже стандартизирован) , Или, что еще лучше, взломайте Numeric Recipes (или поищите его в Интернете) или Knuth и реализуйте один. Они действительно просты, и вам действительно нужно сделать это один раз, чтобы получить универсальный ГСЧ с атрибутами, которые вам чаще всего нужны и которые имеют известное качество.

Томас Каммейер
источник
Спасибо за контекст. На самом деле мне не нужна качественная случайность, и я реализовал MT19937, хотя и в Rust. Больше всего было любопытно узнать, почему две платформы вели себя по-разному.
Терон С
1
Иногда лучшие вопросы задаются из простого интереса, а не из-за строгой необходимости - кажется, что именно они часто дают набор хороших ответов с определенной точки любопытства. Ваш один из них. Это все любопытные люди, настоящие и оригинальные хакеры.
Томас Каммейер
Забавно, что совет заключался в том, чтобы «прекратить использовать rand ()» вместо того, чтобы улучшать rand (). Ничто в стандарте никогда не говорит о том, что это должен быть конкретный генератор.
труба
2
@pipe Если сделать rand()«лучше» означало бы сделать его медленнее (что, вероятно, и так - случайные числа с криптографической защитой требуют больших усилий), то, вероятно, лучше сохранить его быстрым, даже если он будет несколько более предсказуемым. Пример: у нас было производственное приложение, для запуска которого потребовались целые годы, и мы проследили его до ГСЧ, инициализация которого требовала ожидания генерирования достаточной энтропии ... Оказалось, что оно не должно быть настолько безопасным, поэтому заменив его на «худший» ГСЧ был большим улучшением.
gidds