Каков наилучший способ создания случайного двойника на POSIX?

Я хочу получить равномерное распределение в диапазоне [0,0, 1,0)

Если возможно, разрешите реализации использовать случайные байты из /dev/urandom.

Также было бы неплохо, если бы ваше решение было поточно-ориентированным. Если вы не уверены, пожалуйста, укажите это.

См. некоторое решение, о котором я подумал после прочтения других ответов.


person Paweł Hajdan    schedule 28.09.2008    source источник


Ответы (6)


Это кажется довольно хорошим способом:

unsigned short int r1, r2, r3;
// let r1, r2 and r3 hold random values
double result = ldexp(r1, -48) + ldexp(r2, -32) + ldexp(r3, -16);

Это основано на реализации NetBSD drand48.

person Paweł Hajdan    schedule 29.09.2008
comment
Кто-нибудь знает объяснение, почему это дает единый результат в диапазоне 0,0...1,0, где я предполагаю включительно 0,0 и исключительный 1,0? Если да, то это действительно улучшит этот ответ, если его добавить. Было бы очень полезно изменить это, чтобы расширить это. - person E. T.; 22.02.2021
comment
Просто чтобы уточнить, под униформой я подразумеваю отсутствие существенной предвзятости. Обычно это было бы очень важно для большинства приложений, где использование /dev/urandom вместо PRNG, как было задано в исходном вопросе, было бы уместно. - person E. T.; 22.02.2021

Простой: двойной имеет 52-битную точность, предполагающую IEEE. Итак, сгенерируйте 52-битное (или больше) беззнаковое случайное целое число (например, прочитав байты из dev/urandom), преобразуйте его в двойное и разделите на 2 ^ (количество битов, которое было).

Это дает численно равномерное распределение (в том смысле, что вероятность нахождения значения в заданном диапазоне пропорциональна диапазону) вплоть до 52-й двоичной цифры.

Сложно. Однако в диапазоне [0,1] есть много двойных значений, которые вышеописанное не может сгенерировать. Чтобы быть точным, половина значений в диапазоне [0,0,5) (те, у которых установлен младший значащий бит) не могут встречаться. Три четверти значений в диапазоне [0,0,25) (те, у которых установлен любой из их наименьших 2 битов) не могут встречаться и т. д., вплоть до возможного только одного положительного значения меньше 2 ^ -51, несмотря на то, что двойник способен представлять сквиллионы таких значений. Таким образом, нельзя сказать, что он действительно однороден в указанном диапазоне с полной точностью.

Конечно, мы не хотим выбирать один из этих дублей с равной вероятностью, потому что тогда результирующее число в среднем будет слишком маленьким. Нам по-прежнему нужно, чтобы вероятность того, что результат находится в заданном диапазоне, была пропорциональна диапазону, но с более высокой точностью в отношении того, для каких диапазонов это работает.

Я думаю, что работает следующее. Я особо не изучал и не тестировал этот алгоритм (как вы, вероятно, можете сказать по тому, что там нет кода), и лично я бы не стал его использовать, не найдя надлежащих ссылок, указывающих на то, что он действителен. Но вот:

  • Начните экспоненту с 52 и выберите 52-битное случайное целое число без знака (при условии, что мантисса 52 бита).
  • Если старший значащий бит целого числа равен 0, увеличьте показатель степени на единицу, сдвиньте целое число влево на единицу и заполните младший значащий бит новым случайным битом.
  • Повторяйте до тех пор, пока либо вы не нажмете 1 в самом значимом месте, либо показатель степени не станет слишком большим для вашего удвоения (1023. Или, возможно, 1022).
  • Если вы нашли 1, разделите свое значение на 2 ^ показатель степени. Если вы получили все нули, верните 0 (я знаю, что на самом деле это не особый случай, но подчеркивается, насколько маловероятен возврат 0 [Редактировать: на самом деле это может быть особый случай - это зависит от того, хотите ли вы генерировать denorms, Если нет, то, как только у вас будет достаточно 0 в строке, вы отбрасываете все, что осталось, и возвращаете 0. Но на практике это настолько маловероятно, что им можно пренебречь, если только случайный источник не является случайным).

Я не знаю, есть ли на самом деле какое-то практическое применение для такого случайного двойника, заметьте. Ваше определение случайности должно в какой-то степени зависеть от того, для чего оно предназначено. Но если вы можете извлечь выгоду из того, что все 52 его значащих бита являются случайными, это может быть действительно полезно.

person Steve Jessop    schedule 29.09.2008
comment
Я собирался не согласиться с вашим сложным подходом, но как только я прочитал все это, это действительно обрело смысл. Хотя я не эксперт в этом, так что мое мнение не должно учитываться. :-П - person Chris Jester-Young; 29.09.2008
comment
Я тоже - я думаю, что лучший подход - не разрабатывать алгоритмы, требующие случайных поплавков, если вы не уверены, что знаете, что это должно означать. Я не уверен, что делаю... - person Steve Jessop; 29.09.2008

Чтение из файлов является потокобезопасным AFAIK, поэтому использование fopen() для чтения из /dev/urandom даст действительно случайные байты.

Хотя могут быть потенциальные ошибки, мне кажется, что любой набор таких байтов, доступ к которому осуществляется как целое число, разделенное на максимальное целое число этого размера, даст значение с плавающей запятой от 0 до 1 с примерно таким распределением.

Eg:

#include <limits.h>
#include <stdint.h>
#include <stdio.h>
...
FILE* f = fopen("/dev/urandom", "r");
uint32_t i;
fread(&i, sizeof(i), 1, f);  // check return value in real world code!!
fclose(f);
double theRandomValue = i / (double) (UINT32_MAX);
person millenomi    schedule 28.09.2008
comment
Несколько замечаний: (1) Вы не хотите - 1 после 2 ^ 32, так как phjr запросил исключение вывода 1.0. (2) В С++ нет **. Только ‹‹ (используйте 1 ‹‹ 32). (3) Вероятно, вам нужен беззнаковый int. - person Tyler; 29.09.2008
comment
И вы не можете использовать «int» в качестве переменной, потому что это ключевое слово. - person Jonathan Leffler; 29.09.2008

Хитрость в том, что вам нужен 54-битный рандомизатор, отвечающий вашим требованиям. Несколько строк кода с объединением, чтобы вставить эти 54 бита в мантиссу, и вы получите свой номер. Хитрость не в двойном плавании, а в желаемом рандомизаторе.

person old_timer    schedule 28.09.2008
comment
В соответствии с java.util.Random вам нужно 53 бита, а не 54. Прочтите комментарии к методу nextDouble(), чтобы понять, почему. В противном случае вы точно на правильном пути. - person Chris Jester-Young; 29.09.2008
comment
Хотя подход Java не использует объединение для этой цели, он просто создает большое 53-битное число, а затем делит его на (1 ‹‹ 53). - person Chris Jester-Young; 29.09.2008
comment
Это также должно избегать генерации денормализованных значений в 50% случаев, что будет делать объединение. Ваш модуль с плавающей запятой и библиотеки могут корректно обрабатывать денормы, а могут и нет. - person Steve Jessop; 29.09.2008

#include <stdlib.h>
printf("%f\n", drand48());

/dev/случайный:

double c;
fd = open("/dev/random", O_RDONLY);
unsigned int a, b;
read(fd, &a, sizeof(a));
read(fd, &b, sizeof(b));
if (a > b)
   c = fabs((double)b / (double)a);
else
    c = fabs((double)a / (double)b);

c - ваше случайное значение

person Community    schedule 28.09.2008
comment
В этом есть ошибка - если a или b отрицательны, то сравнение не обязательно выбирает значение с меньшей величиной в качестве числителя. Кроме того, я слишком туп, чтобы легко увидеть, что он производит равномерно распределенный вывод. - person Steve Jessop; 28.09.2008
comment
Кроме того, один раз в 2 ^ 64 вы выигрываете двойной джекпот и получаете неопределенное поведение деления на 0. - person Steve Jessop; 29.09.2008
comment
Если подумать, это никогда не бывает однородным. Вы получаете 0, если одно из значений равно 0, что почти равно 1 в 2^31, что намного больше, чем должно быть в двойном с 52-битной точностью мантиссы. - person Steve Jessop; 29.09.2008

/dev/urandom не соответствует POSIX и не является общедоступным.

Стандартный способ равномерного создания двойного числа в [0,1) состоит в том, чтобы сгенерировать целое число в диапазоне [0,2^N) и разделить на 2^N. Так что выберите свой любимый генератор случайных чисел и используйте его. Для моделирования у меня есть Mersenne Twister, так как он очень быстрый, но все еще плохо коррелирует. . На самом деле, он может сделать это за вас и даже имеет версию, которая дает большую точность для меньших чисел. Обычно вы даете ему начальное значение для начала, что помогает повторяемости при отладке или показе другим ваших результатов. Конечно, вы можете сделать так, чтобы ваш код брал случайное число из /dev/urandom в качестве начального числа, если оно не указано.

В криптографических целях вместо этого следует использовать одну из стандартных криптографических библиотек, например openssl), которая действительно используйте /dev/urandom, когда он доступен.

Что касается безопасности потоков, большинство из них не будет, по крайней мере, со стандартными интерфейсами, поэтому вам нужно будет создать слой поверх или использовать их только в одном потоке. В тех, которые являются потокобезопасными, вы предоставляете состояние, которое они изменяют, так что вместо этого вы эффективно запускаете несколько невзаимодействующих генераторов случайных чисел, что может не соответствовать тому, что вы ищете.

person wnoise    schedule 29.09.2008