У меня есть реализация алгоритма xoshiro256** PRNG в приложении, которое я пишу на C#. Это прекрасно работает для создания псевдослучайных значений между 0 и UInt64.MaxValue, но я попал в точку, где мне нужно псевдослучайное значение двойной точности с плавающей запятой между 0 включительно и 1 исключая (то есть между 0 и 0,99999999...).
Я знаю, что могу использовать BitConverter для «грубого преобразования» из ulong в double, но я совершенно уверен, что это даст мне значения, которые находятся где-то между количеством миль в длине планки и количеством кубических миллиметров в Вселенная и их негативы, а также возможность получения бесконечности, отрицательной бесконечности, отрицательного 0 и NaN. Это немного амбициозно (читай: совершенно не подходит) для того, что я пытаюсь сделать, поэтому я надеюсь найти какой-то способ управления выводом, который я получаю, чтобы его можно было использовать для работы с процентными шансами вещей.
Я недостаточно знаю, как работают значения с плавающей запятой IEEE, чтобы точно знать, какие биты куда поместить, чтобы получить значения, которые я ищу. Я думаю (и ошибаюсь), что могу просто сдвинуть вправо ulong на 12 бит (таким образом превратив 52 верхних бита в нижние 52 бита), добавить 2^52 (установив нижний бит экспоненты на 1) и затем BitConverter получившуюся кашу в двойную. Что-то вроде этого:
public static double DoubleFromRand(ulong rand) {
ulong resultUL = rand >> 12;
resultUL += ULongPow(2UL, 52UL); // adapted from https://stackoverflow.com/a/383596/19474638
return BitConverter.ToDouble(BitConverter.GetBytes(resultUL), 0);
}
public static ulong ULongPow(ulong x, ulong pow) {
ulong ret = 1UL;
while (pow != 0UL) {
if ((pow & 1UL) == 1UL) {
ret *= x;
}
x *= x;
pow >>= 1;
}
return ret;
}
Если бы это сработало, я бы ожидал, что передача UInt64.MaxValue в this даст мне какое-то значение, очень близкое к 1, но не совсем там. То, что я на самом деле получаю от приведенного выше алгоритма, — это какое-то очень странное маленькое значение.
Любая подсказка, что делать здесь? Использование C# 4.0 в Mono 6.8.0.105, в 64-разрядной версии Raspberry Pi OS, в Raspberry Pi 4 Model B. Обратите внимание, что меня не интересует использование «настоящего» .NET.
(См.: Как преобразовать uint64_t в двойное/плавающее число между 0 и 1 с максимальной точностью (C++)? Это не отвечает на мой вопрос, поскольку оно преобразует ulong в двойное число с помощью математических операций, что, я считаю, не не гарантируют статистической случайности результата.
Также ответы, касающиеся того, как использовать встроенные случайные функции С#, бесполезны, поскольку я хочу использовать свою собственную реализацию PRNG. Мне также не помогают ответы о том, как генерировать случайные 64-битные числа с плавающей запятой. Я хочу, в частности, взять некоторое количество случайно сгенерированных битов и принудительно преобразовать их в число с плавающей запятой, которое будет находиться между 0 и 1. Вопрос в том, как выполнить преобразование, а не в генерации случайных вещей.)
Не пытайтесь сфабриковать представление о double
. Просто возьмите 52 случайных бита в виде двоичного числа, преобразуйте их в double
(как простое преобразование целого числа в double
) и умножьте на 2 ^ −52, чтобы масштабировать до [0, 1). Это то, что делает приведенный выше код chux, и это нормально для большинства приложений.
@EricPostpischil: На самом деле, вы можете получить 53 бита из-за скрытого битового трюка IEEE 754, дающего вам дополнительную точность.
@EricPostpischil Хм, похоже, что мой код и код chux делают одно и то же, но код chux явно более переносим. Тогда это может сработать.
@dan04: Да, 53 года.
(Редактировать: как указано в комментариях к моему вопросу, это не лучший способ делать что-то, но если у кого-то есть веская причина делать такие вещи, я оставлю это здесь.)
Я думаю, что понял это после некоторых экспериментов.
Поле экспоненты использует значение 1023 для представления 0 — большие значения являются положительными, меньшие — отрицательными. Таким образом, значение 1023 в показателе степени позволяет интерпретировать остальную часть поля мантиссы "как есть", т.е. - все после десятичной точки в числе.
Это приводит к тому, что UInt64.MaxValue
сопоставляется с ~ 1,999, что почти равно 2 (и оно будет напечатано как 2, если вы используете Console.WriteLine()
без установки количества отображаемых цифр). UInt64.MaxValue / 2
сопоставляется с 1.5, UInt64.MaxValue / 4
сопоставляется с 1.25.
Это здорово, за исключением того факта, что ко всем этим значениям прибавлено 1,0. К счастью, похоже, что я могу избавиться от этого, просто вычитая 1,0 из окончательного результата. На первый взгляд кажется, что это работает (UInt64.MaxValue / 4
теперь соответствует 0,25). Я не уверен, что это вызовет ошибки с меньшими значениями, но, надеюсь, этого не произойдет.
Окончательный, я думаю, рабочий код:
public static double DoubleFromRand(ulong rand) {
ulong resultUL = rand >> 12;
resultUL += ((ulong)1023 << 52);
return BitConverter.ToDouble(BitConverter.GetBytes(resultUL), 0) - 1.0;
}
Обратите внимание, что этот беспорядок, вероятно, не переносим на все процессоры - согласно Википедии (где я получил всю информацию о математике с плавающей запятой IEEE754), некоторые процессоры делают действительно странные вещи с порядком байтов при работе с числами с плавающей запятой, поэтому в зависимости от вашего процессора, это может дать вам серьезно зашифрованные результаты. Но пока на моем конкретном 64-битном процессоре ARM это работает.
Общий подход к преобразованию «битов в статистически случайное значение с плавающей запятой от 0 до 1?» начинается с преобразования таких битов в значение между [1,0 ... 2,0) и последующего вычитания 1,0.
Числа с плавающей запятой распределяются линейно между последовательными степенями двойки.
При обычном кодировании double
существует 2 52 значений между большинством степеней 2. Итак, возьмите 64-битные случайные данные и уменьшите до 52 и сформируйте случайное число [1.0... 2.0) (код OP делает это с различные битовые манипуляции), затем вычтите 1,0.
Это подход OP к самоответу .
Когда все сделано правильно, он обеспечивает быстрый подход, но имеет дополнительные проблемы с переносимостью по сравнению со следующим.
Другой подход заключается в использовании 52 из 64 предоставленных случайных битов, формировании значения [0...252-1] и делении на 252. Ожидается, что сдвиг, преобразование из ulong
в double
и деление будут точными и не зависят от режима округления. Таким образом, мы предотвращаем появление статистической погрешности, но формируем только 252 разных значения.
// Something like
double DoubleFromRand(ulong rand) {
return (double)(rand >> 12)/(1ul << 52);
}
Мы можем даже использовать на 1 бит больше @dan04
и по-прежнему иметь «сдвиг, преобразование из ulong
в double
и деление, как ожидается, будут точными и не зависят от режима округления». Использование более 53 бит теряет это свойство.
double DoubleFromRand53(ulong rand) {
return (double)(rand >> 11)/(1ul << 53);
}
Другой подход, не показанный, будет использовать 64 из 64 бит и формировать значение [0...264-1] и делить на 264. К сожалению, это влечет за собой округление целого числа для преобразования и большее округление при делении, что приводит к нежелательному смещению и диапазону (может быть возвращено 1,0), но дает ближе к 264 различным значениям.
Как заметил dan04, мы можем использовать 53 бита. В [½, 1) можно представить 2^52 числа, и мы можем использовать 2^52 числа с тем же интервалом в [0, ½], что дает 2^53 для [0, 1).
@EricPostpischil Да, я тоже сейчас об этом вспоминаю. Ответ изменен.
Попробуйте
double DoubleFromRand(ulong rand) { return (double)(rand >> 12)/(1ul << 52); }
.