Чому rand () повторює номери набагато частіше на Linux, ніж на Mac?


87

Я реалізовував хешмап в C в рамках проекту, над яким я працюю, і використовую випадкові вставки для тестування, коли я помітив, що rand()в Linux, схоже, повторюються цифри набагато частіше, ніж на Mac. RAND_MAXє 2147483647 / 0x7FFFFFFF на обох платформах. Я скоротив її до цієї тестової програми, яка робить байтовий масив RAND_MAX+1-long, генерує 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 послідовно генерує лише одне, тож воно перебирає кожне випадкове число, яке може генерувати майже не повторюючись. Чи може хто-небудь пояснити мені, як це працює? Я не можу розповісти нічого, що відрізняється від чоловічих сторінок, не можу сказати, який RNG використовує кожен, і не можу знайти нічого в Інтернеті. Дякую!


4
Оскільки rand () повертає значення від 0..RAND_MAX включно, ваш масив повинен бути розміром RAND_MAX + 1
Blastfurnace

21
Можливо, ви помітили, що RAND_MAX / e ~ = 790 мільйонів. Також межа (1-1 / n) ^ n у міру наближення до нескінченності дорівнює 1 / е.
Девід Шварц

3
@DavidSchwartz Якщо я вас правильно зрозумів, це може пояснити, чому число в Linux постійно становить близько 790 мільйонів. Я думаю, що тоді питання: чому / як Mac не повторює це багато разів?
Theron S

26
Немає вимоги якості для PRNG в бібліотеці виконання. Єдиною реальною вимогою є повторюваність з тим же насінням. Мабуть, якість PRNG у вашому Linux краще, ніж у вашому Mac.
pmg

4
@chux Так, але оскільки він заснований на множенні, стан ніколи не може бути нульовим, або результат (наступний стан) також буде нульовим. На підставі вихідного коду він перевіряє нуль як особливий випадок, якщо засіяний нулем, але він ніколи не видає нуль як частину послідовності.
Арку

Відповіді:


119

Хоча спочатку це може здатися, що macOS rand()якось краще не повторювати жодних чисел, слід зазначити, що при цій кількості генерованих чисел очікується багато дублікатів (насправді близько 790 мільйонів або (2 31 -1 ) / д ). Так само повторення чисел в послідовності також не дало б дублікатів, але не вважалося б дуже випадковим. Таким чином, Linux rand()реалізація в цьому тесті НЕ відрізнялася від істинного випадкового джерела, в той час як 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 мільйонів дублікатів у вашому тесті.


5
Стаття в журналі, опублікована в 1980-х рр., Запропонувала статистичний тест для PRNG, заснований на "проблемі з днем ​​народження".
pjs

14
"Apple рекламує використання кращих генераторів випадкових чисел у своїй документації" -> звичайно, Apple може використовувати arc4random()подібний код позаду rand()та отримати хороший rand()результат. Замість того, щоб намагатися керувати програмістами, щоб кодувати інакше, просто створіть кращі функції бібліотеки. "вони щойно застрягли" - це їх вибір.
chux

23
відсутність постійного зміщення в mac's rand()робить це так погано, що не корисно для практичного використання: Чому rand ()% 7 завжди повертає 0? , Rand ()% 14 генерує лише значення 6 або 13
phuclv

4
@PeterCordes: Існує така вимога rand, що повторний запуск її з тим же насінням виробляє ту ж послідовність. OpenBSD randпорушено і він не підкоряється цьому договору.
R .. GitHub СТОП ДОПОМОГАТИ ДВІ

8
@ R..GitHubSTOPHELPINGICE Чи бачите ви вимогу C, що rand()з тим самим насінням створюється однакова послідовність між різними версіями бібліотеки? Така гарантія може бути корисною для тесту регресії між бібліотечними версіями, але я не знаходжу вимоги C для цього.
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 біт, який для всіх намірів і цілей ніколи не призведе до повністю повторюваної послідовності. Знову ж таки, ви, напевно, зможете дізнатися, яку версію ви маєте, надрукувавши перші декілька виходів з цього RNG, не висуваючи його попередньо. (Функція glibc rand () видає числа 1804289383, 846930886, 1681692777, 1714636915 та 1957747793.)

Тож причина, у якій ви отримуєте більше зіткнень у Linux (і навряд чи будь-яка в MacOS), полягає в тому, що версія Linux rand () в основному більш випадкова.


5
unseeded 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, з яким пов'язаний, просто збирається в ... яку бібліотеку?
Theron S

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.


Дякую за відповідь. Мені насправді не потрібна хороша PRNG, просто стурбований тим, що в моїй хешмапі ховається якась невизначена поведінка, потім мені стало цікаво, коли я усунув таку можливість і платформи все ще поводилися по-іншому.
Theron S

btw ось приклад криптографічно захищеного генератора випадкових чисел: github.com/divinity76/phpcpp/commit/… - але це C ++ замість C, і я дозволяю
інженерам

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.)

Трохи нижче цього сторінка man насправді дає дуже короткі, дуже прості приклади реалізації rand і srand, які стосуються найпростіших LC RNG, які ви коли-небудь бачили і мають невеликий RAND_MAX. Я не думаю, що вони відповідають тому, що є у стандартній бібліотеці С, якщо вони коли-небудь були. Або принаймні сподіваюся, що ні.

Взагалі, якщо ви збираєтесь використовувати щось із стандартної бібліотеки, використовуйте випадкові, якщо можете (чоловіча сторінка перераховує це як стандарт POSIX назад до POSIX.1-2001, але rand - це стандартний шлях до того, як C навіть був стандартизований) . Або ще краще, зламіть цифрові рецепти (або шукайте їх в Інтернеті) або Knuth і застосуйте його. Вони дійсно прості, і вам дійсно потрібно зробити це один раз, щоб мати загальноприйнятий RNG з атрибутами, які вам найчастіше потрібні і які мають відому якість.


Дякуємо за контекст. Мені насправді не потрібна якісна випадковість, і я реалізував MT19937, хоча і в Русті. В основному було просто цікаво, як з’ясувати, чому дві платформи поводилися по-різному.
Theron S

1
Іноді найкращі запитання задаються з простого інтересу замість суворої потреби - схоже, що найчастіше ті, хто починає набір хороших відповідей з певної цікавості. Ваш - один із них. Ось для всіх допитливих людей, справжніх та оригінальних хакерів.
Томас Каммейер

Смішно, що порада була "припинити використовувати rand ()", а не покращувати rand (). Ніщо в стандарті ніколи не говорить про те, що він повинен бути конкретним генератором.
труба

2
@pipe Якщо зробити rand()«кращим» буде означати зробити його повільніше (що, мабуть, буде - криптографічно захищені випадкові числа потребують великих зусиль), то, ймовірно, краще тримати його швидко, навіть якщо це дещо передбачуваніше. Справа в суті: у нас з'явилася виробнича програма, яка запускала віки, і ми простежили до РНГ, для ініціалізації якого потрібно було дочекатися отримання достатньої ентропії ... Виявилося, вона не повинна бути настільки безпечною, тому замінивши її на «гірший» СПГ був великим поліпшенням.
gidds
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.