Обычно генератор случайных чисел возвращает поток бит, для которого вероятность наблюдения 0 или 1 в каждой позиции равна (то есть 50%). Позвольте называть это непредвзятым PRNG.
Мне нужно сгенерировать строку псевдослучайных битов со следующим свойством: вероятность увидеть 1 в каждой позиции равна р (т.е. вероятность увидеть 0 равна 1-р). Параметр p является действительным числом от 0 до 1; в моей проблеме случается, что она имеет разрешение 0,5%, то есть она может принимать значения 0%, 0.5%, 1%, 1.5%,..., 99.5%, 100%.
Заметим, что p - вероятность, а не точная дробь. Фактическое количество бит, установленное в 1 в потоке из n бит, должно соответствовать биномиальному распределению B (n, p).
Существует наивный метод, который может использовать несмещенный PRNG для генерации значения каждого бита (псевдокода):
generate_biased_stream(n, p):
result = []
for i in 1 to n:
if random_uniform(0, 1) < p:
result.append(1)
else:
result.append(0)
return result
Такая реализация намного медленнее, чем одна, генерирующая несмещенный поток, поскольку она вызывает функцию генератора случайных чисел один раз для каждого бита; в то время как генератор несмещенных потоков вызывает его один раз на размер слова (например, он может генерировать 32 или 64 случайных бита с одним вызовом).
Я хочу более быструю реализацию, даже если она немного приносит в жертву случайность. Идея, которая приходит на ум, состоит в том, чтобы предварительно скопировать таблицу поиска: для каждого из 200 возможных значений p вычислить C 8-битные значения, используя более медленный алгоритм и сохранить их в таблице. Тогда быстрый алгоритм просто выбирал бы один из них случайным образом, чтобы генерировать 8 перекошенных бит.
Задняя часть расчета конверта, чтобы узнать, сколько памяти требуется: C должно быть не менее 256 (количество возможных 8-битных значений), возможно, больше, чтобы избежать эффектов выборки; скажем 1024. Может быть, число должно меняться в зависимости от р, но пусть оно будет простым и сказать, что в среднем 1024. Так как 200 значений p = > общего использования памяти составляет 200 КБ. Это неплохо и может поместиться в кеш L2 (256 КБ). Мне все равно нужно оценить его, чтобы увидеть, есть ли эффекты выборки, которые приводят к смещениям, и в этом случае C нужно будет увеличить.
Недостаток этого решения заключается в том, что он может генерировать только 8 бит одновременно, даже при большой работе, в то время как непредвзятый PRNG может генерировать 64 одновременно с помощью всего лишь нескольких арифметических инструкций.
Я хотел бы знать, существует ли более быстрый метод, основанный на битовых операциях вместо поисковых таблиц. Например, изменение кода генерации случайных чисел непосредственно для введения смещения для каждого бита. Это обеспечило бы такую же производительность, как и непредвзятый PRNG.
Изменить 5 марта
Спасибо всем за ваши предложения, у меня появилось много интересных идей и предложений. Вот верхние:
- Измените требования к проблеме, чтобы p имела разрешение 1/256 вместо 1/200. Это позволяет использовать бит более эффективно, а также дает больше возможностей для оптимизации. Я думаю, что могу внести это изменение.
- Используйте арифметическое кодирование для эффективного использования битов из несмещенного генератора. При вышеуказанном изменении разрешения это становится намного проще.
- Несколько человек предложили, чтобы PRNG были очень быстрыми, поэтому использование арифметического кодирования могло бы сделать код более медленным из-за введенных служебных данных. Вместо этого я должен всегда потреблять наихудшее количество бит и оптимизировать этот код. См. Приведенные ниже тесты.
- @rici предложил использовать SIMD. Это хорошая идея, которая работает только в том случае, если мы всегда потребляем фиксированное количество бит.
Контрольные показатели (без арифметического декодирования)
Примечание: как многие из вас предложили, я изменил разрешение от 1/200 до 1/256.
Я написал несколько реализаций наивного метода, который просто берет 8 случайных несмещенных битов и генерирует 1 смещенный бит:
- Без SIMD
- С SIMD, используя библиотеку векторного вектора Agner Fog, как было предложено @rici
- С SIMD с использованием встроенных функций
Я использую два непредвиденных генератора псевдослучайных чисел:
- xorshift128plus
- Ranvec1 (Mersenne Twister-like) из библиотеки Agner Fog.
Я также измеряю скорость несмещенного PRNG для сравнения. Вот результаты:
RNG: Ranvec1(Mersenne Twister for Graphics Processors + Multiply with Carry)
Method: Unbiased with 1/1 efficiency, SIMD=vectorclass (incorrect, baseline)
Gbps/s: 16.081 16.125 16.093 [Gb/s]
Number of ones: 536,875,204 536,875,204 536,875,204
Theoretical : 104,857,600
Method: Biased with 1/8 efficiency
Gbps/s: 0.778 0.783 0.812 [Gb/s]
Number of ones: 104,867,269 104,867,269 104,867,269
Theoretical : 104,857,600
Method: Biased with 1/8 efficiency, SIMD=vectorclass
Gbps/s: 2.176 2.184 2.145 [Gb/s]
Number of ones: 104,859,067 104,859,067 104,859,067
Theoretical : 104,857,600
Method: Biased with 1/8 efficiency, SIMD=intrinsics
Gbps/s: 2.129 2.151 2.183 [Gb/s]
Number of ones: 104,859,067 104,859,067 104,859,067
Theoretical : 104,857,600
SIMD повышает производительность в 3 раза по сравнению со скалярным методом. Это в 8 раз медленнее, чем ожидаемый генератор.
Самый быстрый смещенный генератор достигает 2,1 Гбит/с.
RNG: xorshift128plus
Method: Unbiased with 1/1 efficiency (incorrect, baseline)
Gbps/s: 18.300 21.486 21.483 [Gb/s]
Number of ones: 536,867,655 536,867,655 536,867,655
Theoretical : 104,857,600
Method: Unbiased with 1/1 efficiency, SIMD=vectorclass (incorrect, baseline)
Gbps/s: 22.660 22.661 24.662 [Gb/s]
Number of ones: 536,867,655 536,867,655 536,867,655
Theoretical : 104,857,600
Method: Biased with 1/8 efficiency
Gbps/s: 1.065 1.102 1.078 [Gb/s]
Number of ones: 104,868,930 104,868,930 104,868,930
Theoretical : 104,857,600
Method: Biased with 1/8 efficiency, SIMD=vectorclass
Gbps/s: 4.972 4.971 4.970 [Gb/s]
Number of ones: 104,869,407 104,869,407 104,869,407
Theoretical : 104,857,600
Method: Biased with 1/8 efficiency, SIMD=intrinsics
Gbps/s: 4.955 4.971 4.971 [Gb/s]
Number of ones: 104,869,407 104,869,407 104,869,407
Theoretical : 104,857,600
Для xorshift SIMD увеличивает производительность в 5 раз по сравнению со скалярным методом. Он в 4 раза медленнее, чем несмещенный генератор. Обратите внимание, что это скалярная реализация xorshift.
Самый быстрый смещенный генератор достигает 4,9 Гбит/с.
RNG: xorshift128plus_avx2
Method: Unbiased with 1/1 efficiency (incorrect, baseline)
Gbps/s: 18.754 21.494 21.878 [Gb/s]
Number of ones: 536,867,655 536,867,655 536,867,655
Theoretical : 104,857,600
Method: Unbiased with 1/1 efficiency, SIMD=vectorclass (incorrect, baseline)
Gbps/s: 54.126 54.071 54.145 [Gb/s]
Number of ones: 536,874,540 536,880,718 536,891,316
Theoretical : 104,857,600
Method: Biased with 1/8 efficiency
Gbps/s: 1.093 1.103 1.063 [Gb/s]
Number of ones: 104,868,930 104,868,930 104,868,930
Theoretical : 104,857,600
Method: Biased with 1/8 efficiency, SIMD=vectorclass
Gbps/s: 19.567 19.578 19.555 [Gb/s]
Number of ones: 104,836,115 104,846,215 104,835,129
Theoretical : 104,857,600
Method: Biased with 1/8 efficiency, SIMD=intrinsics
Gbps/s: 19.551 19.589 19.557 [Gb/s]
Number of ones: 104,831,396 104,837,429 104,851,100
Theoretical : 104,857,600
Эта реализация использует AVX2 для одновременного запуска 4 несмещенных генераторов xorshift.
Самый быстрый смещенный генератор достигает 19,5 Гбит/с.
Тесты для арифметического декодирования
Простые тесты показывают, что арифметический код декодирования является узким местом, а не PRNG. Поэтому я только сравниваю самый дорогой PRNG.
RNG: Ranvec1(Mersenne Twister for Graphics Processors + Multiply with Carry)
Method: Arithmetic decoding (floating point)
Gbps/s: 0.068 0.068 0.069 [Gb/s]
Number of ones: 10,235,580 10,235,580 10,235,580
Theoretical : 10,240,000
Method: Arithmetic decoding (fixed point)
Gbps/s: 0.263 0.263 0.263 [Gb/s]
Number of ones: 10,239,367 10,239,367 10,239,367
Theoretical : 10,240,000
Method: Unbiased with 1/1 efficiency (incorrect, baseline)
Gbps/s: 12.687 12.686 12.684 [Gb/s]
Number of ones: 536,875,204 536,875,204 536,875,204
Theoretical : 104,857,600
Method: Unbiased with 1/1 efficiency, SIMD=vectorclass (incorrect, baseline)
Gbps/s: 14.536 14.536 14.536 [Gb/s]
Number of ones: 536,875,204 536,875,204 536,875,204
Theoretical : 104,857,600
Method: Biased with 1/8 efficiency
Gbps/s: 0.754 0.754 0.754 [Gb/s]
Number of ones: 104,867,269 104,867,269 104,867,269
Theoretical : 104,857,600
Method: Biased with 1/8 efficiency, SIMD=vectorclass
Gbps/s: 2.094 2.095 2.094 [Gb/s]
Number of ones: 104,859,067 104,859,067 104,859,067
Theoretical : 104,857,600
Method: Biased with 1/8 efficiency, SIMD=intrinsics
Gbps/s: 2.094 2.094 2.095 [Gb/s]
Number of ones: 104,859,067 104,859,067 104,859,067
Theoretical : 104,857,600
Метод простой фиксированной точки достигает 0,25 Гбит/с, а наивный скалярный метод - в 3 раза быстрее, а наивный метод SIMD - в 8 раз быстрее. Могут быть способы дальнейшей оптимизации и/или параллелизации метода арифметического декодирования, но из-за его сложности я решил остановиться здесь и выбрать наивную реализацию SIMD.
Спасибо всем за помощь.