Ищете PRNG приличного качества только с 32 битами состояния

Я пытаюсь реализовать версию интерфейса rand_r сносного качества, которая имеет неудачное требование интерфейса, согласно которому все его состояние хранится в одном объекте типа unsigned, что для моих целей означает ровно 32 бита. Кроме того, мне нужно, чтобы его выходной диапазон был [0,2³¹-1]. Стандартным решением является использование LCG и отбрасывание младшего бита (который имеет самый короткий период), но это все равно оставляет очень плохие периоды для следующих нескольких битов.

Моя первоначальная мысль состояла в том, чтобы использовать две или три итерации LCG для генерации высоких/низких или высоких/средних/младших битов вывода. Однако такой подход не сохраняет несмещенного распределения; вместо того, чтобы каждое выходное значение имело одинаковую частоту, многие из них встречаются несколько раз, а некоторые вообще никогда не возникают.

Поскольку существует только 32 бита состояния, период PRNG ограничен 2³², и, чтобы быть непредвзятым, PRNG должен выводить каждое значение ровно дважды, если оно имеет полный период, или ровно один раз, если оно имеет период 2³¹. Более короткие периоды не могут быть беспристрастными.

Есть ли какой-нибудь известный алгоритм PRNG, отвечающий этим критериям?


person R.. GitHub STOP HELPING ICE    schedule 11.06.2013    source источник
comment
гарантированный вывод кажется противоположным случайному, не так ли?   -  person xaxxon    schedule 11.06.2013
comment
Псевдослучайность не случайна. А противоположность гарантированного результата, к сожалению, предвзята. Для безумно больших периодов, таких как 2 ^ 100, идеальное распределение выходных данных на самом деле не обязательно, если оно эмпирически неотличимо от равномерного, но для небольшого периода, такого как 2 ^ 32, неравномерность будет явным наблюдаемым смещением.   -  person R.. GitHub STOP HELPING ICE    schedule 11.06.2013
comment
Да, вы можете предсказать следующее значение PRNG с периодом 2 ^ 32, если у вас есть примерно 0,5 ГБ данных о предыдущих значениях, которые он вернул. Этого следовало ожидать, и это было бы так, даже если бы распределение не было равномерным. Если имеется ровно 2^32 состояния и 2^32 выхода, и вы видели 2^32-1 из них, всегда определяется последнее.   -  person R.. GitHub STOP HELPING ICE    schedule 11.06.2013
comment
Я удалил свой комментарий. Я не совсем понял, что вы сказали, но, кажется, теперь понял.   -  person xaxxon    schedule 11.06.2013
comment
Как насчет использования LCG, а затем применения безопасного преобразования 1:1 (я предложил несколько здесь), чтобы улучшить распределение ? В принципе, это похоже на запуск криптографического алгоритма в режиме счетчика.   -  person sh1    schedule 11.06.2013
comment
Я знаю, что здесь вы достаточно ясно говорите «32-бит», но если вы полностью прикованы к соответствующей платформе, не могли бы вы просто сохранить указатель на malloc() любого практичного размера? Или индекс в статическую таблицу указателей?   -  person sh1    schedule 11.06.2013
comment
@sh1: Хорошая попытка, но нет, требование интерфейса состоит в том, чтобы состояние полностью сохранялось в объекте unsigned int. Однако преобразование выходных данных LCG или LFSR с картой масштаба 1:1 звучит более многообещающе.   -  person R.. GitHub STOP HELPING ICE    schedule 12.06.2013


Ответы (4)


Одним из хороших (но, вероятно, не самых быстрых) вариантов, предлагающих очень высокое качество, было бы использование 32-битного блока . шифр в режиме CTR. По сути, ваше состояние RNG будет просто 32-битным счетчиком, который увеличивается на единицу при каждом вызове RNG, а результатом будет шифрование этого значения счетчика с использованием блочного шифра с некоторым произвольно выбранным фиксированным ключом. Для дополнительной случайности вы можете даже предоставить (нестандартную) функцию, позволяющую пользователю установить собственный ключ.

Существует не так много широко используемых 32-битных блочных шифров, поскольку такой короткий размер блока создает проблемы для криптографического использования. (По сути, парадокс дня рождения позволяет отличить вывод такого шифра от случайной функции с непренебрежимо малая вероятность после примерно 216 = 65536 выходов, а после 232 выходов неслучайность, очевидно, становится очевидной.) Однако некоторые шифры с регулируемым размером блока , например XXTEA или HPC позволит вам перейти на 32-битную версию и подойдет для ваших целей.

(Редактировать: Плохо, XXTEA использует только 64-разрядную версию. Однако, как предлагает CodesInChaos в комментариях, Skip32 может быть другим вариантом. Или вы можете создать свой собственный 32-разрядный шифр Фейстеля.)

Конструкция режима CTR гарантирует, что RNG будет иметь полный период из 232 выходов, в то время как стандартное требование безопасности (не взломанных) блочных шифров по существу состоит в том, что вычислительно невозможно различить их выходные данные. из случайной перестановки набора 32-битных целых чисел. (Конечно, как отмечалось выше, такую ​​перестановку легко отличить от случайной функции принимает 32-битные значения.)

Использование режима CTR также предоставляет некоторые дополнительные функции, которые могут оказаться удобными (даже если они не являются частью официального API, для которого вы разрабатываете), такие как возможность быстрого поиска в любой точке выходного потока RNG, просто добавляя или вычитание из состояния.

С другой стороны, вы, вероятно, не хотите следовать общепринятой практике заполнения RNG, просто устанавливая внутреннее состояние в начальное значение, так как это приведет к тому, что выходные потоки, сгенерированные из ближайших начальных значений, быть очень похожими (в основном просто один и тот же поток, сдвинутый из-за разницы семян). Один из способов избежать этой проблемы — добавить дополнительный шаг шифрования в процесс заполнения, т. е. зашифровать начальное число с помощью шифра и установить значение внутреннего счетчика равным результату.

person Ilmari Karonen    schedule 11.06.2013
comment
Проблема с этим подходом заключается в обеспечении полного периода. Очевидно, что это можно было бы проверить эмпирически, учитывая примерно день вычислительного времени, но я не вижу очевидных причин полагать, что применение криптографического шифра менее 2³² раз не вернет исходное значение. - person R.. GitHub STOP HELPING ICE; 11.06.2013
comment
Конструкция режима CTR явно гарантирует полный период: выходной поток будет повторяться только тогда, когда счетчик зацикливается. - person Ilmari Karonen; 11.06.2013
comment
О, я вижу. К сожалению, в API нет процесса заполнения; семя и состояние одинаковы. Это очень, очень плохой API, поэтому я так ограничен. С учетом сказанного, я думаю, что это может сработать, чтобы зашифровать входное начальное число/состояние, использовать полученное значение в качестве результата, увеличить это значение на 1, затем «расшифровать» его и сохранить результат обратно в начальное число/состояние. - person R.. GitHub STOP HELPING ICE; 11.06.2013
comment
Вы также можете заменить счетчик в режиме CTR обобщенным счетчиком, то есть любой последовательностью 32-битных значений с полным периодом (например, LCRNG с полным периодом). Таким образом, вы, по сути, будете использовать блочный шифр для усиления более простого ГСЧ с полным периодом путем шифрования его вывода. Базовый простой LCRNG по-прежнему должен быть достаточно случайным, чтобы избежать проблем с последовательными начальными значениями. - person Ilmari Karonen; 11.06.2013
comment
согласно Википедии XXTEA имеет минимальный размер блока 64 бита. Вместо этого вы можете рассмотреть Skip32. - person CodesInChaos; 11.06.2013
comment
Минимальный блок для XXTEA, как написано, кажется 64-битным (2 32-битных слова). Имеет ли смысл просто отказаться от алгоритма, использующего пару 16-битных слов, и соответствующим образом настроить размер ключа и размер расписания ключей? Для криптографического использования, очевидно, потребуется много анализа, но для использования PRNG я думаю, что это было бы безопасно, и количество раундов тоже можно было бы значительно сократить, не так ли? - person R.. GitHub STOP HELPING ICE; 11.06.2013
comment
@R: Да, это может сработать. Очевидно, что вам следует прогнать полученный ГСЧ через обычную батарею тестов (DIEHARD, TestU01 и т. д.). Или попробуйте Skip32, предложенный CodesInChaos. - person Ilmari Karonen; 11.06.2013
comment
Пока ваш ответ кажется наиболее теоретически обоснованным, но, возможно, непрактичным с точки зрения производительности. Я держу все варианты открытыми на данный момент. Приятно иметь три принципиально разных ответа, которые выглядят многообещающе. - person R.. GitHub STOP HELPING ICE; 12.06.2013
comment
Шифр Hasty Pudding имеет очень широкий диапазон размеров блоков, включая 32 бита. - person rossum; 12.06.2013

Уточняю мой комментарий...

Блочный шифр в режиме счетчика дает генератор примерно в следующем виде (за исключением использования гораздо больших типов данных):

uint32_t state = 0;
uint32_t rand()
{
    state = next(state);
    return temper(state);
}

Поскольку криптографическая защита не указана (а в 32-битной системе это было бы более или менее бесполезно), более простая специальная функция закалки должна сработать.

Один подход состоит в том, что функция next() проста (например, return state + 1;), а temper() компенсируется сложностью (как в блочном шифре).

Более сбалансированный подход состоит в том, чтобы реализовать LCG в next(), поскольку мы знаем, что он также посещает все возможные состояния, но в случайном порядке, и найти реализацию temper(), которая выполняет ровно столько работы, сколько необходимо для решения оставшихся проблем с LCG.

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

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

person sh1    schedule 11.06.2013
comment
Я не пробовал бит-реверсирование, но пока байтовый обмен кажется многообещающим. LFSR выглядит более многообещающе в качестве «следующей» функции, чем LCG, поскольку LCG имеет действительно плохой период в младших битах, тогда как LFSR имеет полный период во всех битах. - person R.. GitHub STOP HELPING ICE; 12.06.2013
comment
@R.., Идея состоит в том, что temper() будет распространять вещи, чтобы биты плохого периода были рассредоточены. У LCG просто есть простое преимущество: он работает на полный период без каких-либо дополнительных сложностей. Но, на самом деле, если вы все-таки использовали умножение в темперировании, то, наверное, к лучшему, если вы используете принципиально другой алгоритм для next(). Так что, пожалуй, я с вами соглашусь. - person sh1; 12.06.2013
comment
Я только что попробовал простую LCG с функцией Tempor, скопированной из Mersenne Twister, и стойкие результаты статистически неотличимы от /dev/urandom. :-) - person R.. GitHub STOP HELPING ICE; 12.06.2013
comment
@R.., Затем тест сложнее! - person sh1; 12.06.2013
comment
@R.., ты пробовал более сложные тесты? Я не ожидаю, что они обнаружат что-то новое, но это кажется слишком простым; как будто несгибаемый что-то проглядел. - person sh1; 27.06.2013
comment
Я старался изо всех сил, да. Никаких сбоев. Я не пробовал testu01 (слишком много работы по настройке, и я еще не дошел до этого), но у меня был кто-то в нашей команде с опытом работы с PRNG, который провел некоторые другие статистические тесты с методами Монте-Карло, и единственные тесты это потерпели неудачу те, которые, как доказуемо, должны потерпеть неудачу в течение столь коротких периодов времени. - person R.. GitHub STOP HELPING ICE; 27.06.2013

Вам может подойти 32-битный LFSR Галуа с максимальным периодом. Пытаться:

r = (r >> 1) ^ (-(r & 1) & 0x80200003);

Единственная проблема с LFSR заключается в том, что вы не можете получить значение 0. Таким образом, этот имеет диапазон от 1 до 2 ^ 32-1. Вы можете настроить вывод или использовать хороший LCG.

person Lee Daniel Crocker    schedule 11.06.2013
comment
Возможно if (r==0xdeadbeef) r=0; else if (r==0) r=0xdeadbeef; r = (r >> 1) ^ (-(r & 1) & 0x80200003); - person R.. GitHub STOP HELPING ICE; 11.06.2013
comment
Лучше не возиться с самой переменной состояния. Просто используйте r-1 в качестве выходного значения. Это делает ваш диапазон и период от 0 до 2 ^ 32-2. - person Lee Daniel Crocker; 11.06.2013
comment
Да, но я не могу изменить диапазон. Это фиксированная. Таким образом, мне нужно вставить 0 в произвольной точке последовательности. - person R.. GitHub STOP HELPING ICE; 11.06.2013
comment
Недавно я немного подумал об этом подходе в отношении Multiply With Carry (здесь это не подходящее решение), так как я хотел соединить две независимые большие орбиты, переключая треки между ними. Я решил, что идеальной идиомой будет if ((x ^ 0xdeadbeef) == 0 || x == 0) x = x ^ 0xdeadbeef;, так как это приводит к очень компактному коду (я имею в виду скомпилированный код) только с одним условным блоком. - person sh1; 12.06.2013
comment
Я не уверен, что понимаю, что вы здесь пытаетесь. Это в точности то же самое, что и первый комментарий R ( ((x ^ z) == 0) совпадает с (x == z) ), а LFSR никогда равен 0. - person Lee Daniel Crocker; 12.06.2013
comment
Это ноль, если мы сделаем его нулевым. Хитрость заключается в том, чтобы прыгать в нулевое состояние и выходить из него каждые 2 ^ 31-1 итерации. Таким образом, ноль добавляется к набору выходов, расширяя его точно до 2^32 и делая его совершенно беспристрастным. 0xdeadbeef — это просто произвольное значение, после которого в поток вставляется 0 (неважно, запускаем ли мы генератор, так как ноль переходит в ноль). В следующий раз, когда мы увидим, что состояние равно нулю, мы прыгнем обратно в поток в то же место, откуда вышли в прошлый раз, и продолжим, как будто ничего не произошло. Мое предложенное изменение просто предназначено для повышения вычислительной эффективности. - person sh1; 12.06.2013
comment
А, теперь я понял. Вы вставляете 0 в последовательность в произвольной точке. Прохладный. Мне потребовалось некоторое время. - person Lee Daniel Crocker; 12.06.2013
comment
Это дает вам диапазон от 1 до 2 ^ 32-1, OP требуется от 0 до 2 ^ 31-1, поэтому просто выведите r››1. Замена произвольного значения на 0 по-прежнему дает вам дыру в вашем дистрибутиве, что звучит не очень умно. - person Michel Rouzic; 29.09.2013
comment
@MichelRouzic Он не заменяет значение нулем, он вставляет ноль в последовательность после этого значения, давая последовательность длиной 2 ^ 32. Это на самом деле умно. - person sam hocevar; 13.09.2016
comment
Вы уверены, что максимум 2^32? Я, кажется, получаю примерно половину этого. - person Ash; 03.09.2018
comment
Ах, неважно, я нашел свою проблему. Он использовал целые числа без знака и искажал отрицательную часть алгебры. - person Ash; 03.09.2018

Помимо использования Lehmer MCG, вы можете использовать еще пару:

32-разрядные варианты Xorshift имеют гарантированный период 2 32−1 с использованием 32-битного состояния:

uint32_t state;

uint32_t xorshift32(void) {
    state ^= state << 13;
    state ^= state >> 17;
    state ^= state << 5;
    return state;
}

Это исходная 32-битная рекомендация от 2003 года (см. статью). В зависимости от вашего определения «приличного качества», это должно быть хорошо. Однако он не прошел бинарные ранговые тесты Diehard и 5/10 тестов SmallCrush.

Альтернативная версия с улучшенным микшированием и константами (уступает SmallCrush и Crush):

uint32_t xorshift32amx(void) {
    int s = __builtin_bswap32(state * 1597334677);
    state ^= state << 13;
    state ^= state >> 17;
    state ^= state << 5;
    return state + s;
}

На основе исследований здесь и здесь.


Есть также Mulberry32 с периодом ровно 2 32:

uint32_t mulberry32(void) {
    uint32_t z = state += 0x6D2B79F5;
    z = (z ^ z >> 15) * (1 | z);
    z ^= z + (z ^ z >> 7) * (61 | z);
    return z ^ z >> 14;
}

Это, вероятно, ваш лучший вариант. Это очень хорошо. Автор заявляет: «Он проходит 13 тестов Gjrand без сбоев и общего значения P 0,984 (где 1 идеально, а 0,1 или меньше - сбой) на 4 ГБ сгенерированных данных. Это четверть полного периода». Похоже, это улучшение по сравнению с SplitMix32.


"SplitMix32", заимствованный из xxHash/MurmurHash3 (последовательность Вейля):

uint32_t splitmix32(void) {
    uint32_t z = state += 0x9e3779b9;
    z ^= z >> 15; // 16 for murmur3
    z *= 0x85ebca6b;
    z ^= z >> 13;
    z *= 0xc2b2ae35;
    return z ^= z >> 16;
}

Качество здесь может быть сомнительным, но у его 64-битного старшего брата множество поклонников (проходит BigCrush). Так что стоит посмотреть на общую структуру.

person bryc    schedule 28.08.2018
comment
Есть мысли о семействе PCG? (Например, генератор под названием PCG RXS M XS 32 (LCG) в документе PCG.) - person Mark Dickinson; 28.08.2018
comment
@MarkDickinson Из того, что я видел, PCG полагается на 64-битную математику, и я не могу найти полностью 32-битных вариантов. Я также не могу найти какой-либо код, специфичный для генератора, о котором вы упомянули. Я уверен, что PCG — хороший генератор, но мне кажется, что это плохой выбор для JavaScript или встраиваемых систем. xoshiro128**, например, идеально подходит в этом отношении, поскольку использует 32-битные целые числа. только. - person bryc; 28.08.2018
comment
Имеет смысл; благодаря. Конечно, вы можете эмулировать необходимую 64-битную арифметику, но это повлияет на сложность и эффективность кода. - person Mark Dickinson; 28.08.2018
comment
Код для splitmix32 не имеет периода 2^32. Он имеет фиксированную точку 0xe1aff528! Кроме того, mulberry32 также не имеет периода 2^32 (хотя, по крайней мере, у него нет фиксированных точек). - person Ben Karel; 25.04.2020
comment
Вы уверены насчет Mulberry32? Автор утверждает, что у него должен быть период от 2 до 32, поскольку перед циклом состояние меняется на каждое 32-битное целое число без знака.. какой у тебя тестовый код? - person bryc; 26.04.2020