Быстрый, беспристрастный, целочисленный псевдослучайный генератор с произвольными границами

Для процесса интеграции Монте-Карло мне нужно потянуть много случайных выборок из
гистограмма, которая имеет N сегментов и где N произвольно (то есть не имеет степени двойки), но
не меняется вообще во время вычисления.

По много, Я имею в виду что-то порядка 10 ^ 10, 10 миллиардов, так что почти любой
вид длинного предварительного вычисления, вероятно, стоит того перед лицом огромного числа
Образцы).

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

Наивный способ вытащить образец: histogram[ prng() % histogram.size() ]

Наивный путь очень медленно: операция по модулю использует целочисленное деление (IDIV)
который ужасно дорогой и компилятор, не зная значения histogram.size()
во время компиляции не может быть в соответствии с его обычной магией (т.е. http://www.azillionmonkeys.com/qed/adiv.html)

Фактически, большая часть моего вычислительного времени потрачена на извлечение этого проклятого по модулю.

Немного менее наивный способ: я использую libdivide (http://libdivide.com/) который способен
выполнения очень быстрого «деления на константу, не известную во время компиляции».

Это дает мне очень хороший выигрыш (25% или около того), но у меня есть ноющее чувство, что я могу сделать
лучше вот почему:

  • Первая интуиция: libdivide вычисляет деление. Что мне нужно, это по модулю, и чтобы добраться туда
    Я должен сделать дополнительный мульт и саб: mod = dividend - divisor*(uint64_t)(dividend/divisor), Я подозреваю, что там может быть небольшая победа, используя libdivide-type
    методы, которые производят по модулю напрямую.

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

Модуль является довольно стандартным способом добраться из-за двух его свойств:

  • A) mod(prng(), N) гарантированно будет равномерно распределен, если prng() является

  • B) mod(prgn(), N) гарантированно принадлежит [0, N [

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

Все, что нужно, это функция, любой функция, которая подчиняется ограничениям A) и B) и является быстро.

Итак, длинное вступление, но вот два моих вопроса:

  • Есть ли что-то эквивалентное libdivide, которое вычисляет целое число по модулю непосредственно ?

  • Существует ли некоторая функция F (X, N) целых чисел X и N, которая подчиняется следующим двум ограничениям:

    • Если X является случайной величиной, равномерно распределенной, то F (X, N) также неравномерно распределенный
    • F (X, N) гарантированно находится в [0, N [

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

Редактировать : prng() % N действительно не совсем равномерно распределены. Но для N достаточно большой, я не думаю, что это большая проблема (или это?)

Изменить 2: prng() % N действительно потенциально очень плохо распределен. Я никогда не понимал, как плохо это может быть. Уч. Я нашел хорошую статью по этому поводу: http://ericlippert.com/2013/12/16/how-much-bias-is-introduced-by-the-remainder-technique

3

Решение

Если у вас есть быстрый доступ к необходимой инструкции, вы можете умножить 64-битные prng() от N и вернуть старшие 64 бита 128-битного результата. Это как умножение равномерного действительного числа в [0, 1) на N и усечение, с уклоном порядка порядка по модулю (то есть практически незначительным; 32-битная версия этого ответа будет иметь небольшое, но, возможно, заметное смещение).

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

2

Другие решения

В данных обстоятельствах самый простой подход может работать лучше всего. Один чрезвычайно простой подход, который может сработать, если ваш PRNG достаточно быстрый, состоит в том, чтобы предварительно вычислить на единицу меньше, чем следующая большая степень 2, чем ваш N, чтобы использовать в качестве маски. Т.е., учитывая некоторое число, которое выглядит как 0001xxxxxxxx в двоичном формате (где x означает, что нам все равно, если это 1 или 0) мы хотим маску, как 000111111111,

Оттуда мы генерируем числа следующим образом:

  1. Генерация числа
  2. and это с вашей маской
  3. если результат> n, переходите к 1

Точная эффективность этого будет зависеть от того, насколько N близко к степени 2. Каждая последующая степень 2 (очевидно, достаточно) удваивает свою предшественницу. Таким образом, в лучшем случае N ровно на единицу меньше, чем степень 2, и наш тест на шаге 3 всегда проходит. Мы добавили только маску и сравнение со временем, затраченным на сам PRNG.

В худшем случае N в точности равно степени 2. В этом случае мы ожидаем выбросить примерно половину сгенерированных нами чисел.

В среднем N оказывается примерно посередине между степенями 2. Это означает, что в среднем мы отбрасываем примерно один из четырех входов. Мы можем почти игнорировать маску и сами сравнения, поэтому наша потеря скорости по сравнению с «необработанным» генератором в основном равна количеству его выходов, которые мы отбрасываем, или 25% в среднем.

3

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

  1. убедитесь, что размер таблицы равен степени двух (добавьте отступы, если необходимо!)

  2. замените операцию по модулю битовой маской. Как это:

    size_t tableSize = 1 << 16;
    size_t tableMask = tableSize - 1;
    
    ...
    
    histogram[prng() & tableMask]
    

Операция с битовой маской — это один цикл на любом процессоре, который стоит своих денег, вы не можете превзойти его скорость.

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

size_t bitCount = 16;

...

histogram[prng() >> (64 - bitCount)]

Это так же быстро, как битовая маска, но она использует разные биты.

1

Вы могли бы продлить histogram до «большой» степени двойки путем его циклического заполнения, заполняя конечные пробелы некоторой фиктивной величиной (гарантированно никогда не встречающейся в реальных данных). Например. с учетом гистограммы

[10, 5, 6]

растяните его на длину 16 примерно так (при условии -1 является подходящим стражем):

[10, 5, 6, 10, 5, 6, 10, 5, 6, 10, 5, 6, 10, 5, 6, -1]

Затем выборка может быть выполнена с помощью бинарной маски histogram[prng() & mask] где mask = (1 << new_length) - 1с проверкой дозорного значения для повторения, то есть

int value;
do {
value = histogram[prng() & mask];
} while (value == SENTINEL);

// use `value` here

Расширение является более длинным, чем необходимо, чтобы сделать повторные попытки маловероятными, гарантируя, что подавляющее большинство элементов являются действительными (например, в приведенном выше примере только 1/16 поисков «потерпят неудачу», и эта скорость может быть дополнительно уменьшена путем расширения ее, например, до 64 ). Вы могли бы даже использовать подсказку «предсказание ветвления» (например, __builtin_expect в GCC) на проверку, чтобы компилятор заказывал код как оптимальный для случая, когда value != SENTINEL, что, надеюсь, является общим случаем.

Это очень большой компромисс между памятью и скоростью.

1

Просто несколько идей, чтобы дополнить другие хорошие ответы:

  1. Какой процент времени тратится на операцию по модулю, и как вы узнаете, что это за процент? Я спрашиваю только потому, что иногда люди говорят, что что-то ужасно медленное, хотя на самом деле это происходит менее чем в 10% случаев, и они думают, что это что-то большое, потому что они используют глупый профайлер, работающий только на время. (Мне трудно представить себе операцию по модулю, отнимающую много времени по сравнению с генератором случайных чисел.)

  2. Когда станет известно количество ведер? Если это не меняется слишком часто, вы можете написать программу-генератор. Когда количество блоков меняется, автоматически распечатайте новую программу, скомпилируйте, создайте ссылку и используйте ее для массового выполнения.
    Таким образом, компилятор будет знать количество сегментов.

  3. Рассматривали ли вы использование генератор квазислучайных чисел, в отличие от псевдослучайного генератора? Это может дать вам более высокую точность интеграции в гораздо меньшем количестве образцов.

  4. Может ли быть уменьшено количество сегментов без чрезмерного снижения точности интеграции?

1

Неоднородность, о которой предупреждает dbaupp, может быть отклонена путем отклонения&перерисовка значений не менее M*(2^64/M) (прежде чем взять модуль).
Если M может быть представлен не более чем в 32 битах, вы можете получить более чем на одно значение меньше M повторным умножением (см. ответ Давида Эйзенстата) или divmod; в качестве альтернативы, вы можете использовать битовые операции, чтобы выделить битовые шаблоны достаточно долго для Mснова отклоняя значения не менее M,
(Я был бы удивлен тем, что модуль генерации случайных чисел не карликов по времени / циклу / потреблению энергии.)

1

Чтобы кормить ведро, вы можете использовать std::биномиальное распределение напрямую кормить каждое ведро вместо того, чтобы кормить одно ведро одним образцом:

Следующее может помочь:

int nrolls = 60; // number of experiments
const std::size_t N = 6;
unsigned int bucket[N] = {};

std::mt19937 generator(time(nullptr));

for (int i = 0; i != N; ++i) {
double proba = 1. / static_cast<double>(N - i);
std::binomial_distribution<int> distribution (nrolls, proba);
bucket[i] = distribution(generator);
nrolls -= bucket[i];
}

Живой пример

0

Вместо целочисленного деления вы можете использовать математику с фиксированной запятой, т.е. целочисленное умножение & Bitshift. Скажем, если ваш prng () возвращает значения в диапазоне 0-65535, и вы хотите, чтобы это квантовалось в диапазоне 0-99, тогда вы делаете (prng () * 100) >> 16. Просто убедитесь, что умножение не переполняет ваш целочисленный тип, поэтому вам, возможно, придется сдвинуть результат prng () вправо. Обратите внимание, что это отображение лучше, чем по модулю, поскольку оно сохраняет равномерное распределение.

0
По вопросам рекламы [email protected]