2015-08-15 4 views
10

Ho eseguito il test con l'implementazione twister Mersenne della libreria standard gcc C++. Supera sia il generatore a congruenza lineare che il C rand, che è probabilmente un LCG. Anche il A boost documentation sembra dare un risultato simile, ma favorisce ancora di più il twister di Mersenne. Qualcuno può spiegarlo?Perché Mersenne è più veloce del generatore a congruenza lineare?

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

class Timer 
{ 
private: 
    std::chrono::high_resolution_clock::time_point start_time; 
    std::chrono::high_resolution_clock::time_point stop_time; 

public: 
    void start() 
    { 
    start_time = std::chrono::high_resolution_clock::now(); 
    } 

    void stop() 
    { 
    stop_time = std::chrono::high_resolution_clock::now(); 
    } 

    double measure() 
    { 
    using namespace std::chrono; 
    return duration_cast<microseconds> 
    (stop_time - start_time).count()/1000000.0; 
    } 
}; 

template<typename T> 
class Random 
{ 
private: 
    T generator; 

public: 
    Random() 
    : generator 
    (std::chrono::high_resolution_clock::now().time_since_epoch().count()) 
    { 
    } 

    int generate_integer(int begin, int end) 
    { 
    return std::uniform_int_distribution<int>(begin, end - 1)(generator); 
    } 
}; 

int main() 
{ 
    constexpr int n = 300000000; 
    Random<std::minstd_rand> mr; 
    Random<std::mt19937> mt; 
    Timer t; 
    for (int j = 0; j < 3; ++j) 
    { 
    t.start(); 
    for (int i = 0; i < n; ++i) 
    { 
     static_cast<volatile void>(mr.generate_integer(0, 10)); 
    } 
    t.stop(); 
    std::cout << "minstd " << t.measure() << std::endl; 
    t.start(); 
    for (int i = 0; i < n; ++i) 
    { 
     static_cast<volatile void>(mt.generate_integer(0, 10)); 
    } 
    t.stop(); 
    std::cout << "mersenne " << t.measure() << std::endl; 
    t.start(); 
    for (int i = 0; i < n; ++i) 
    { 
     static_cast<volatile void>(std::rand() % 10); 
    } 
    t.stop(); 
    std::cout << "rand " << t.measure() << std::endl; 
    } 
} 

risultato

minstd 4.70876 
mersenne 1.55853 
rand 4.11873 
minstd 4.53199 
mersenne 1.55928 
rand 4.15159 
minstd 4.5374 
mersenne 1.55667 
rand 4.13715 
+1

Ti aspettavi un risultato diverso? – emlai

+2

@zenith Certo, un LCG è solo alcune operazioni aritmetiche, mentre mt19937 è di diverse pagine di codice. – xiver77

+1

Hai attivato le ottimizzazioni? Per buoni risultati è necessario attivare ottimizzazioni complete e forzare un effetto collaterale (accumulando un checksum?) Dell'attività temporizzata come stampare un risultato di checksum * dopo * che il timer si è fermato per impedire al compilatore di ottimizzare l'attività temporizzata. – Galik

risposta

6

L'algoritmo di Mersenne Twister non è complicato come sembra. O, più precisamente, quasi tutta la parte complicata non viene eseguita abbastanza spesso da incidere seriamente sulla velocità media a lungo termine.

Se si guarda allo pseudocode implementation on Wikipedia, la stragrande maggioranza delle chiamate esercita solo la seconda metà della funzione extract_number(); il resto del codice di non inizializzazione (principalmente nella funzione twist()) viene eseguito solo in una chiamata in 625 (nella versione più comune). La parte che viene eseguita ogni volta è molto semplice, solo una manciata di turni e altre operazioni bit a bit, che si prevede possano essere molto veloci nella maggior parte dei processori. Il test all'inizio di extract_number() è quasi sempre falso e quindi può essere facilmente ottimizzato mediante la previsione dei rami.

Questo contrasto con l'algoritmo congruenziale lineare, in cui ogni chiamata fa un Moltiplica numeri interi (costoso) e la divisione modulo (molto costoso, a meno che non imbrogliare utilizzando una potenza di 2 modulo, che incide la qualità della vostra casuale numeri). L'aritmetica coinvolta negli algoritmi LC e MT è così diversa che non sono sorpreso se le loro prestazioni relative variano da un sistema all'altro, ma non ho difficoltà a credere che MT sia più veloce in almeno alcuni casi.

(Se si guarda da vicino l'algoritmo MT, sembra a prima vista di fare diverse operazioni di rollover per ogni iterazione in twist(), ma quelli sono in forme che sono facili da ottimizzare la via.)

Per quanto riguarda la pianura vecchio rand(), le implementazioni di questo variano ampiamente e non dovrebbe essere previsto per essere coerente tra i sistemi. Molte implementazioni usano l'aritmetica a 16 bit e sarebbero naturalmente più veloci degli algoritmi a 32 o 64 bit.

3

non riesco a riprodurre i risultati, quando ho provato rand sembra molto più veloce

[email protected] ~/cpp/test5 $ g++ -std=c++11 main.cpp -o main 
[email protected] ~/cpp/test5 $ ./main 
minstd 18.168 
mersenne 20.7626 
rand 3.13027 
minstd 17.8153 
mersenne 20.8395 
rand 3.19297 
minstd 18.0667 
mersenne 20.7672 
rand 3.13617 

Edit: quando lo faccio con -O3, rand è ancora più veloce

[email protected] ~/cpp/test5 $ g++ -std=c++11 -O3 main.cpp -o main 
[email protected] ~/cpp/test5 $ ./main 
minstd 7.74432 
mersenne 8.54915 
rand 3.04077 
minstd 7.73824 
mersenne 8.5711 
rand 3.03335 
minstd 7.74818 
mersenne 8.55403 
rand 3.03481 

Penso che sia probabilmente OS/compilatore/configurazione dipendente? Forse su Windows, la chiamata a std :: rand() richiede implicitamente di recuperare l'ora dal sistema operativo o qualcosa per inizializzarla, o qualcosa del genere? (Edit: io non sono sicuro di capire i risultati spinta, però, e dubito che i risultati spinta rifletterebbero un problema del genere)

Il mio sistema operativo e compilatore:

[email protected] ~/cpp/test5 $ cat /etc/issue 
Linux Mint 17.1 Rebecca \n \l 

[email protected] ~/cpp/test5 $ gcc -v 
Using built-in specs. 
COLLECT_GCC=gcc 
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/4.8/lto-wrapper 
Target: x86_64-linux-gnu 
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 4.8.4-2ubuntu1~14.04' --with-bugurl=file:///usr/share/doc/gcc-4.8/README.Bugs --enable-languages=c,c++,java,go,d,fortran,objc,obj-c++ --prefix=/usr --program-suffix=-4.8 --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --with-gxx-include-dir=/usr/include/c++/4.8 --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --enable-gnu-unique-object --disable-libmudflap --enable-plugin --with-system-zlib --disable-browser-plugin --enable-java-awt=gtk --enable-gtk-cairo --with-java-home=/usr/lib/jvm/java-1.5.0-gcj-4.8-amd64/jre --enable-java-home --with-jvm-root-dir=/usr/lib/jvm/java-1.5.0-gcj-4.8-amd64 --with-jvm-jar-dir=/usr/lib/jvm-exports/java-1.5.0-gcj-4.8-amd64 --with-arch-directory=amd64 --with-ecj-jar=/usr/share/java/eclipse-ecj.jar --enable-objc-gc --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --with-tune=generic --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu 
Thread model: posix 
gcc version 4.8.4 (Ubuntu 4.8.4-2ubuntu1~14.04) 

Edit: ho fatto di nuovo con "-fwhole-programma", non cambia molto:

[email protected] ~/cpp/test5 $ g++ -std=c++11 -fwhole-program -O3 main.cpp -o main 
[email protected] ~/cpp/test5 $ ./main 
minstd 8.15607 
mersenne 8.03688 
rand 2.9622 
minstd 8.17983 
mersenne 7.99626 
rand 2.90655 
minstd 8.16007 
mersenne 7.99331 
rand 2.90902 
4

Questo è probabilmente perché rand sta accedendo locale di thread per recuperare il suo stato.

Ho provato questo utilizzando Visual Studio 2015 Community e ottenuto risultati simili a OP. Osservando il sorgente per rand fornito con il compilatore VS2012, rand() accede alla memoria locale del thread per ottenere il valore precedente, che viene quindi passato attraverso la matematica LCRG per generare il successivo.

L'utilizzo della mia versione di rand senza l'accesso all'archivio locale mi dà un tempo più veloce - circa 0,25 su scala dell'OP.