Случайные разности двигателей

В стандарте С++ 11 задается ряд различных движков для генерации случайных чисел: linear_congruential_engine, mersenne_twister_engine, subtract_with_carry_engine и т.д. Очевидно, это большое изменение от старого использования std::rand.

Очевидно, что одним из основных преимуществ (по крайней мере, некоторых) этих двигателей является увеличенная длина периода (он встроен в имя для std::mt19937).

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

Ответ 1

Из приведенных ниже объяснений линейный двигатель кажется более быстрым, но менее случайным, в то время как Marsenne Twister имеет более высокую сложность и случайность. Механизм случайных чисел с вычитанием с переносом является улучшением линейного двигателя, и он определенно более случайный. В последней ссылке говорится, что Mersenne Twister имеет более высокую сложность, чем двигатель случайных чисел Subtract-with-carry

Линейный конгруэнтный механизм случайных чисел

Механизм генерации псевдослучайных чисел, который генерирует целые числа без знака.

Это самый простой механизм генератора в стандартной библиотеке. Его состояние представляет собой одно целое значение со следующим алгоритмом перехода:

x = (ax + c) mod m

Где x - текущее значение состояния, a и c являются их соответствующими параметрами шаблона, а m является его соответствующим параметром шаблона, если это больше, чем 0, или числами_limits:: max() плюс 1, в противном случае.

Его алгоритм генерации является прямой копией значения состояния.

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

Случайные числа, порожденные linear_congruential_engine, имеют период m. http://www.cplusplus.com/reference/random/linear_congruential_engine/

Двигатель случайных чисел Мерсинн твистер

Механизм генерации псевдослучайных чисел, который генерирует целые числа без знака в закрытом интервале [0,2 ^ w-1].

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

Двигатель имеет внутреннюю последовательность состояний из n целочисленных элементов, которая заполняется псевдослучайным рядом, генерируемым при построении, или путем вызова семантической функции члена.

Внутренняя последовательность состояний становится источником для n элементов. Когда состояние продвинуто (например, для создания нового случайного числа), двигатель изменяет последовательность состояний, скручивая текущее значение с помощью xor mask a на сочетание бит, определяемое параметром r, которое исходит от этого значения и от значения m элементов (подробнее см. оператор()).

Полученные случайные числа - это закаленные версии этих скрученных значений. Отклонение представляет собой последовательность операций сдвига и xor, определяемых параметрами u, d, s, b, t, c и l, примененными к выбранному значению состояния (см. Оператор()).

Случайные числа, генерируемые mersenne_twister_engine, имеют период, эквивалентный числу mersenne 2 ^ ((n-1) * w) -1. http://www.cplusplus.com/reference/random/mersenne_twister_engine/

Механизм случайных чисел с вычитанием с переносом

Механизм генерации псевдослучайных чисел, который генерирует целые числа без знака.

Алгоритм, используемый этим движком, представляет собой отстающий генератор фибоначчи с последовательностью состояний из г целых элементов плюс одно значение переноса. http://www.cplusplus.com/reference/random/subtract_with_carry_engine/

Отложенные генераторы Фибоначчи имеют максимальный период (2k - 1) * ^ (2M-1), если используется сложение или вычитание. Инициализация LFG является очень сложной задачей. Выход LFG очень чувствителен к начальным условиям, и статистические дефекты могут появляться первоначально, но также периодически в выходной последовательности, если не будет предпринята особая осторожность. Другая потенциальная проблема с LFG заключается в том, что математическая теория, стоящая за ними, является неполной, что делает необходимым полагаться на статистические тесты, а не на теоретические характеристики. http://en.wikipedia.org/wiki/Lagged_Fibonacci_generator

И наконец: Выбор того, какой двигатель использовать, включает в себя ряд компромиссов: линейный конгруэнтный двигатель умеренно быстр и имеет очень небольшое требование к хранилищу для состояния. Отложенные генераторы Фибоначчи очень быстрые даже на процессорах без усовершенствованных наборов арифметических команд за счет большего государственного хранения и иногда менее желательных спектральных характеристик. Misterenne twister работает медленнее и имеет большие требования к хранению данных, но с правильными параметрами имеет самую длинную неповторяющуюся последовательность с наиболее желательными спектральными характеристиками (для данного определения желаемого). в http://en.cppreference.com/w/cpp/numeric/random

Ответ 2

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

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

В зависимости от вашей потребности вы можете взять один генератор или другой. Например, если вам нужны быстрые случайные числа, но на самом деле не важно качество, LCG может быть хорошим вариантом. Если вам нужны более качественные случайные числа, Mersenne Twister, вероятно, лучший вариант.

Чтобы помочь вам сделать свой выбор, есть несколько стандартных тестов и результатов (мне определенно нравится таблица p.29 this бумага).


ИЗМЕНИТЬ: Из статьи

  • Семейство LCG (LCG(***) в документе) - самые быстрые генераторы, но с самым низким качеством.
  • Mersenne Twister (MT19937) немного медленнее, но дает лучшие случайные числа.
  • Выдержка с переносом (SWB(***), я думаю) медленнее, но при правильной настройке может давать лучшие случайные свойства.

Ответ 3

Как другие ответы забывают о ranlux, вот небольшая заметка разработчика AMD, которая недавно портировала его в OpenCL:

http://devgurus.amd.com/thread/139236

RANLUX также является одним из немногих (единственное, что я знаю на самом деле) PRNG, у которого есть основополагающая теория, объясняющая, почему она генерирует "случайные" числа и почему они хороши. Действительно, если теория верна (и я не знаю никого, кто ее оспаривал), RANLUX на самом высоком уровне роскоши производит полностью декоррелированные номера до последнего бита, без корреляций на большие расстояния, пока мы остаемся хорошо ниже периода (10 ^ 171). Большинство других генераторов могут сказать очень мало о своем качестве (например, Mersenne Twister, KISS и т.д.). Они должны полагаться на прохождение статистических тестов.

Физики в CERN являются поклонниками этого PRNG. - сказал он.

Ответ 4

Некоторая информация из этих других ответов противоречит моим выводам. Я запускал тесты в Windows 8.1 с помощью Visual Studio 2013, и, как правило, я обнаружил, что mersenne_twister_engine имеет более высокое качество и значительно быстрее, чем linear_congruential_engine или subtract_with_carry_engine. Это заставляет меня поверить, когда информация в других ответах учитывается, что конкретная реализация движка оказывает значительное влияние на производительность.

Это никого не удивляет, я уверен, но это не упоминается в других ответах, где mersenne_twister_engine говорят, что он медленнее. У меня нет результатов тестов для других платформ и компиляторов, но с моей конфигурацией mersenne_twister_engine, безусловно, является лучшим выбором при рассмотрении периода, качества и скорости работы. Я не профилировал использование памяти, поэтому я не могу говорить с требованием к пространству.

Здесь код, который я использую для тестирования (чтобы сделать переносной, вам нужно будет только заменить вызовы API windows.h QueryPerformanceXxx() соответствующим механизмом синхронизации):

// compile with: cl.exe /EHsc
#include <random> 
#include <iostream>
#include <windows.h>

using namespace std;

void test_lc(const int a, const int b, const int s) {
    /*
    typedef linear_congruential_engine<unsigned int, 48271, 0, 2147483647> minstd_rand;
    */
    minstd_rand gen(1729);

    uniform_int_distribution<> distr(a, b);

    for (int i = 0; i < s; ++i) {
        distr(gen);
    }
}

void test_mt(const int a, const int b, const int s) {
    /*
    typedef mersenne_twister_engine<unsigned int, 32, 624, 397,
    31, 0x9908b0df,
    11, 0xffffffff,
    7, 0x9d2c5680,
    15, 0xefc60000,
    18, 1812433253> mt19937;
    */
    mt19937 gen(1729);

    uniform_int_distribution<> distr(a, b);

    for (int i = 0; i < s; ++i) {
        distr(gen);
    }
}

void test_swc(const int a, const int b, const int s) {
    /*
    typedef subtract_with_carry_engine<unsigned int, 24, 10, 24> ranlux24_base;
    */
    ranlux24_base gen(1729);

    uniform_int_distribution<> distr(a, b);

    for (int i = 0; i < s; ++i) {
        distr(gen);
    }
}

int main()
{
    int a_dist = 0;
    int b_dist = 1000;

    int samples = 100000000;

    cout << "Testing with " << samples << " samples." << endl;

    LARGE_INTEGER ElapsedTime;
    double        ElapsedSeconds = 0;

    LARGE_INTEGER Frequency;
    QueryPerformanceFrequency(&Frequency);
    double TickInterval = 1.0 / ((double) Frequency.QuadPart);

    LARGE_INTEGER StartingTime;
    LARGE_INTEGER EndingTime;
    QueryPerformanceCounter(&StartingTime);
    test_lc(a_dist, b_dist, samples);
    QueryPerformanceCounter(&EndingTime);
    ElapsedTime.QuadPart = EndingTime.QuadPart - StartingTime.QuadPart;
    ElapsedSeconds = ElapsedTime.QuadPart * TickInterval;
    cout << "linear_congruential_engine time: " << ElapsedSeconds << endl;

    QueryPerformanceCounter(&StartingTime);
    test_mt(a_dist, b_dist, samples);
    QueryPerformanceCounter(&EndingTime);
    ElapsedTime.QuadPart = EndingTime.QuadPart - StartingTime.QuadPart;
    ElapsedSeconds = ElapsedTime.QuadPart * TickInterval;
    cout << "   mersenne_twister_engine time: " << ElapsedSeconds << endl;

    QueryPerformanceCounter(&StartingTime);
    test_swc(a_dist, b_dist, samples);
    QueryPerformanceCounter(&EndingTime);
    ElapsedTime.QuadPart = EndingTime.QuadPart - StartingTime.QuadPart;
    ElapsedSeconds = ElapsedTime.QuadPart * TickInterval;
    cout << "subtract_with_carry_engine time: " << ElapsedSeconds << endl;
}

Вывод:

Testing with 100000000 samples.
linear_congruential_engine time: 10.0821
   mersenne_twister_engine time: 6.11615
subtract_with_carry_engine time: 9.26676

Ответ 5

Его компромисс действительно. PRNG, как Mersenne Twister, лучше, потому что он имеет чрезвычайно большой период и другие хорошие статистические свойства.

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

Выберите PNRG в зависимости от потребностей вашего приложения. Если у вас есть сомнения, используйте Mersenne Twister, его значение по умолчанию используется во многих инструментах.

Ответ 6

Я только что увидел этот ответ от Marnos и решил проверить его сам. Я использовал std::chono::high_resolution_clock для времени 100000 samples 100 times для получения среднего значения. Я измерил все в std::chrono::nanoseconds и получил разные результаты:

std::minstd_rand имела среднее значение 28991658 наносекунд

std::mt19937 имела среднее значение 29871710 наносекунд

ranlux48_base имела среднее значение 29281677 наносекунд

Это на компьютере с Windows 7. Компилятор - Mingw-Builds 4.8.1 64bit. Это, очевидно, использует флаг С++ 11 и флаги оптимизации.

Когда я включаю оптимизацию -O3, std::minstd_rand и ranlux48_base работают быстрее, чем может быть реализована реализация high_precision_clock; однако std::mt19937 по-прежнему принимает наночастицы 730045 или 3/4 секунды.

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

В стороне я использовал механизм Mersenne Twister в своей библиотеке генерации шума (он не прекомпретирует градиенты), поэтому я думаю, что переключусь на один из других, чтобы действительно увидеть некоторые улучшения скорости. В моем случае "истинная" случайность не имеет значения.

код:

#include <iostream>
#include <chrono>
#include <random>

using namespace std;
using namespace std::chrono;

int main()
{
    minstd_rand linearCongruentialEngine;
    mt19937 mersenneTwister;
    ranlux48_base subtractWithCarry;
    uniform_real_distribution<float> distro;

    int numSamples = 100000;
    int repeats = 100;

    long long int avgL = 0;
    long long int avgM = 0;
    long long int avgS = 0;

    cout << "results:" << endl;

    for(int j = 0; j < repeats; ++j)
    {
        cout << "start of sequence: " << j << endl;

        auto start = high_resolution_clock::now();
        for(int i = 0; i < numSamples; ++i)
            distro(linearCongruentialEngine);
        auto stop = high_resolution_clock::now();
        auto L = duration_cast<nanoseconds>(stop-start).count();
        avgL += L;
        cout << "Linear Congruential:\t" << L << endl;

        start = high_resolution_clock::now();
        for(int i = 0; i < numSamples; ++i)
            distro(mersenneTwister);
        stop = high_resolution_clock::now();
        auto M = duration_cast<nanoseconds>(stop-start).count();
        avgM += M;
        cout << "Mersenne Twister:\t" << M << endl;

        start = high_resolution_clock::now();
        for(int i = 0; i < numSamples; ++i)
            distro(subtractWithCarry);
        stop = high_resolution_clock::now();
        auto S = duration_cast<nanoseconds>(stop-start).count();
        avgS += S;
        cout << "Subtract With Carry:\t" << S << endl;
    }

    cout << setprecision(10) << "\naverage:\nLinear Congruential: " << (long double)(avgL/repeats)
    << "\nMersenne Twister: " << (long double)(avgM/repeats)
    << "\nSubtract with Carry: " << (long double)(avgS/repeats) << endl;
}

Ответ 7

В общем, mersenne twister - лучший (и самый быстрый) RNG, но для этого требуется некоторое пространство (около 2,5 килобайт). Какой из них подходит вам, зависит от того, сколько раз вам нужно создать экземпляр объекта генератора. (Если вам нужно создать экземпляр только один раз или несколько раз, тогда используется MT. Если вам нужно создать экземпляр его миллионы раз, то, возможно, что-то меньшее.)

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

Я провел небольшую программу для проверки скорости разных генераторов и их размеров и получил следующее:

std::mt19937 (2504 bytes): 1.4714 s
std::mt19937_64 (2504 bytes): 1.50923 s
std::ranlux24 (120 bytes): 16.4865 s
std::ranlux48 (120 bytes): 57.7741 s
std::minstd_rand (4 bytes): 1.04819 s
std::minstd_rand0 (4 bytes): 1.33398 s
std::knuth_b (1032 bytes): 1.42746 s