2015-08-15 10 views
10

Testowałem przy użyciu implementacji Twister Mersenne w bibliotece standardowej gcc C++. Przewyższa zarówno liniowy generator kongruencji, jak i C rand, co jest najprawdopodobniej LCG. A boost documentation również wydaje się dawać podobny wynik, ale faworyzując twister Mersenne jeszcze bardziej. Czy ktoś może to wyjaśnić?Dlaczego tornado Mersenne jest szybsze niż liniowy generator kongruencji?

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

wynik

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

Czy oczekiwałeś innego wyniku? – emlai

+2

@zenith Pewnie, LCG to tylko kilka operacji arytmetycznych, a mt19937 ma kilka stron kodu. – xiver77

+1

Czy włączono optymalizacje? Aby uzyskać dobre wyniki, należy włączyć pełne optymalizacje i wymusić efekt uboczny (sumowanie sumy kontrolnej?) Działania czasowego, np. Wydrukowanie wyniku sumy kontrolnej * po * zatrzymaniu stopera, aby uniemożliwić kompilatorowi optymalizację działania czasowego. – Galik

Odpowiedz

6

Algorytm Mersenne Twister nie jest tak skomplikowany, jak wygląda. Lub, dokładniej, prawie cała skomplikowana część nie jest wykonywana wystarczająco często, aby poważnie wpłynąć na długoterminową średnią prędkość.

Jeśli spojrzeć na pseudocode implementation on Wikipedia, większość połączeń wykonuje tylko drugą połowę funkcji extract_number(); pozostała część kodu nieinicjującego (głównie w funkcji twist()) działa tylko w jednym wywołaniu w 625 (w najpopularniejszej wersji). Część uruchamiana za każdym razem jest bardzo prosta, wystarczy kilka zmian i inne operacje bitowe, które mogą być bardzo szybkie na większości procesorów. Test na początku extract_number() jest prawie zawsze fałszywy i można go łatwo zoptymalizować za pomocą prognozy rozgałęzień.

Kontrast to z liniowego algorytmu przystający, w którym każda rozmowa robi się pomnożyć całkowitą (drogie) i dzielenie modulo (bardzo drogie, chyba że oszukują przy użyciu potęgi 2 moduł, który ma wpływ na jakość Twojego losowo liczby). Arytmetyka zaangażowana w algorytmy LC i MT jest tak różna, że ​​nie jestem zaskoczona, jeśli ich względna wydajność różni się w zależności od systemu, ale nie mam problemu z przekonaniem, że MT jest co najmniej w niektórych przypadkach szybsze.

(Jeśli przyjrzeć algorytmu MT, wydaje się na pierwszy rzut oka, że ​​robi kilka modulo operacji na iteracji w twist(), ale te są w formach, które są łatwe do optymalizacji dala.)

chodzi o zwykły stare rand(), implementacje tego są bardzo różne i nie należy oczekiwać, że będą spójne w różnych systemach. Wiele implementacji wykorzystuje 16-bitową arytmetykę i naturalnie byłoby szybsze niż 32- lub 64-bitowe algorytmy.

3

I nie można odtworzyć swoje wyniki, gdy próbuję go rand pojawia się znacznie szybciej

[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 

Edycja: Kiedy zrobić z -O3, Rand jest nadal szybszy

[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 

Myślę, że to prawdopodobnie zależy od systemu operacyjnego/kompilatora/konfiguracji? Być może w systemie Windows wywołanie std :: rand() niejawnie musi pobrać czas z systemu operacyjnego lub coś, co go zasieje, lub coś w tym stylu? (Edit: Nie jestem pewien, czy rozumiem wyniki Boost choć i wątpię wyniki Boost będzie odzwierciedlać problem tak)

Mój OS i kompilatora:

[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: zrobiłem to jeszcze raz z „-fwhole-programem”, nie zmieniły się znacząco:

[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

jest to prawdopodobnie dlatego rand korzysta pamięć lokalna wątku, aby odzyskać swój stan.

Próbowałem tego przy użyciu społeczności Visual Studio 2015 i otrzymałem wyniki podobne do OP. Patrząc na źródło randu dostarczone z kompilatorem VS2012, rand() uzyskuje dostęp do lokalnej pamięci wątku, aby uzyskać poprzednią wartość, która następnie jest przekazywana przez matematykę LCRG do generowania następnej.

Używanie własnej wersji Rand bez lokalnego dostępu do magazynu daje mi szybszy czas - około 0,25 na skali OP.

Powiązane problemy