2015-08-15 11 views
10

Ich habe mit der Mersenne Twister-Implementierung der gcc C++ - Standardbibliothek getestet. Es übertrifft sowohl den linearen Kongruenzgenerator als auch die C rand, die höchstwahrscheinlich eine LCG ist. A boost documentation scheint auch ein ähnliches Ergebnis zu geben, aber Mersenne twister noch mehr zu bevorzugen. Kann das jemand erklären? Warum ist Mersenne schneller als der lineare Kongruenzgenerator?

#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; 
    } 
} 

Ergebnis

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

Haben Sie ein anderes Ergebnis erwartet? – emlai

+2

@zenith Sicher, eine LCG ist nur ein paar arithmetische Operationen, während mt19937 mehrere Seiten Code enthält. – xiver77

+1

Haben Sie Optimierungen aktiviert? Für gute Ergebnisse sollten Sie volle Optimierungen einschalten und einen Nebeneffekt (Akkumulieren einer Prüfsumme?) Der zeitgesteuerten Aktivität erzwingen, wie das Ausdrucken eines Prüfsummenergebnisses * nachdem der Timer gestoppt wurde, um zu verhindern, dass der Compiler die zeitgesteuerte Aktivität weg optimiert. – Galik

Antwort

6

Der Mersenne Twister-Algorithmus ist nicht so kompliziert wie es aussieht. Genauer gesagt wird fast der gesamte komplizierte Teil nicht oft genug ausgeführt, um die langfristige Durchschnittsgeschwindigkeit ernsthaft zu beeinflussen.

Wenn Sie sich die pseudocode implementation on Wikipedia betrachten, üben die überwiegende Mehrheit der Anrufe nur die zweite Hälfte der extract_number()-Funktion; Der Rest des Nicht-Initialisierungscodes (hauptsächlich in der twist()-Funktion) läuft nur in einem Aufruf in 625 (in der gebräuchlichsten Version). Der Teil, der jedes Mal ausgeführt wird, ist sehr einfach, nur eine Handvoll von Verschiebungen und anderen bitweisen Operationen, von denen erwartet werden kann, dass sie auf den meisten Prozessoren sehr schnell sind. Der Test zu Beginn von extract_number() ist fast immer falsch und kann daher leicht durch Verzweigungsvorhersage optimiert werden.

Kontrast dies mit dem linearen Kongruenz-Algorithmus, bei dem jeder Anruf tut eine ganze Zahl multiplizieren (teuer) und Modulo-Division (sehr teuer, es sei denn, Sie betrügen, indem eine Leistung von 2 Modul verwenden, welche Auswirkungen der Qualität Ihrer zufälligen Nummern). Die Arithmetik, die in den LC- und MT-Algorithmen involviert ist, ist so unterschiedlich, dass ich nicht überrascht bin, wenn ihre relative Leistung von System zu System variiert, aber ich habe keine Probleme zu glauben, dass MT zumindest in einigen Fällen schneller ist.

(Wenn Sie genau an der MT-Algorithmus sucht, erscheint es auf den ersten Blick mehrere Modulo-Operationen pro Iteration in twist() zu tun, aber die sind in Formen, die sich leicht optimieren sind.)

Wie für Normal alt rand(), die Implementierungen davon sind sehr unterschiedlich und es sollte nicht erwartet werden, dass sie systemübergreifend konsistent sind. Viele Implementierungen verwenden eine 16-Bit-Arithmetik und wären natürlich schneller als 32- oder 64-Bit-Algorithmen.

3

ich Ihre Ergebnisse nicht reproduzieren kann, wenn ich es rand versuchen scheint viel schneller

[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: Wenn ich es mit O3 tun, Rand ist immer noch schneller

[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 

Ich denke, es ist wahrscheinlich OS/Compiler/Konfiguration abhängig? Vielleicht unter Windows, Aufruf von std :: rand() implizit muss die Zeit vom Betriebssystem oder etwas, um es zu säen, oder etwas Ähnliches? (Edit: Ich bin nicht sicher, ob ich die Boost-Ergebnisse verstehen aber, und ich bezweifle, dass die Boost-Ergebnisse ein Problem wie das widerspiegeln würde)

Mein OS und Compiler:

[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: Ich tat es wieder ändern mit „-fwhole-Programm“, nicht viel:

[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

Dies ist wahrscheinlich, weil rand ist Thread-Lokalspeicher Zugriff auf seinen Zustand zu erhalten.

Ich versuchte dies mit Visual Studio 2015 Community und bekam Ergebnisse ähnlich OP. Mit Blick auf die Quelle für den mit dem VS2012-Compiler gelieferten Rand greift rand() auf den lokalen Thread-Speicher zu, um den vorherigen Wert zu erhalten, der dann durch die LCRG-Mathematik weitergegeben wird, um den nächsten zu erzeugen.

Mit meiner eigenen Version von Rand ohne den lokalen Speicherzugriff gibt mir eine Zeit viel schneller - etwa 0,25 auf OP-Skala.

Verwandte Themen