为什么Mersenne Twister比线性同余生成器更快?

13

我测试了gcc C++标准库的Mersenne twister实现。它比线性同余生成器和C语言的rand()函数表现更优秀,后者很可能是一个LCG(线性同余生成器)。Boost文档似乎也给出了类似的结果,但更青睐于Mersenne twister。有人能解释一下吗?

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

结果

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
你期望得到不同的结果吗? - emlai
4
当然,LCG只需要几个算术运算,而mt19937则需要数页的代码。 - user3810155
1
你是否开启了优化?为了获得良好的结果,你应该开启完整的优化,并强制产生一个副作用(累加校验和?)在计时活动结束后打印出校验结果,以防止编译器优化掉计时活动。 - Galik
1
@Galik 测试使用了 -O3 -fwhole-program 运行。由于 static_cast<volatile void>,编译器没有优化掉这些语句。 - user3810155
@xiver77 噢,好的,学到了 static_cast<volatile void> 这个技巧! - Galik
3个回答

9
Mersenne Twister算法并不像看起来那么复杂。或者更准确地说,几乎所有复杂的部分都没有执行到足够频繁以严重影响长期平均速度。
如果你查看维基百科上的伪代码实现,绝大多数调用只使用了extract_number()函数的后半部分;其余的非初始化代码(主要在twist()函数中)仅在625次调用中运行一次(在最常见的版本中)。每次运行的部分非常简单,只有少量移位和其他按位操作,在大多数处理器上可以期望非常快。在extract_number()的开头进行的测试几乎总是错误的,因此可以通过分支预测轻松优化。
与线性同余算法相比,梅森旋转算法中每次调用只进行整数乘法(代价高)和模除法(非常昂贵,除非您通过使用2的幂模来作弊,这会影响您的随机数的质量)。LC和MT算法涉及的算术操作如此不同,以至于它们的相对性能可能因系统而异,但我毫不怀疑,在至少某些情况下,MT更快。

(如果您仔细观察MT算法,乍一看似乎在每次迭代中执行多个模操作twist(),但这些形式易于优化。)

至于普通的rand(),其实现各不相同,不应期望在各个系统之间保持一致。许多实现使用16位算术,自然比32或64位算法更快。


5

这可能是因为rand访问线程本地存储来检索其状态。

我尝试过在Visual Studio 2015 Community上使用此方法,并获得了类似于OP的结果。查看提供给VS2012编译器的rand的源代码,rand()访问线程本地存储以获取先前的值,然后通过LCRG数学生成下一个值。

使用我自己的没有本地存储访问的rand版本,可以使运行时间更快 - 大约是OP规模的0.25。


3

我无法复现你的结果,当我尝试时,rand 函数似乎更快。

chris@chris-thinkpad ~/cpp/test5 $ g++ -std=c++11  main.cpp -o main
chris@chris-thinkpad ~/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

编辑:当我使用 -O3 进行编译时,rand 函数仍然更快。
chris@chris-thinkpad ~/cpp/test5 $ g++ -std=c++11 -O3 main.cpp -o main
chris@chris-thinkpad ~/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

我认为这可能与操作系统/编译器/配置有关?也许在Windows上,调用std::rand()隐含地必须从操作系统获取时间或其他内容来进行种子初始化,或者类似的操作?(编辑:虽然我不确定是否理解了boost的结果,但我怀疑boost的结果是否反映了这样的问题)

我的操作系统和编译器:

chris@chris-thinkpad ~/cpp/test5 $ cat /etc/issue
Linux Mint 17.1 Rebecca \n \l

chris@chris-thinkpad ~/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) 

编辑:我再次使用了“-fwhole-program”,但效果并不大:

chris@chris-thinkpad ~/cpp/test5 $ g++ -std=c++11 -fwhole-program -O3 main.cpp -o main
chris@chris-thinkpad ~/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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接