所以我看到一个名为 rand() 被认为有害 的演讲,它提倡使用随机数生成的引擎分布范式,而不是简单的 std::rand()
加模范式。
但是,我想亲眼看看 std::rand()
的失败之处,所以我做了一个快速实验:
- Basically, I wrote 2 functions
getRandNum_Old()
andgetRandNum_New()
that generated a random number between 0 and 5 inclusive usingstd::rand()
andstd::mt19937
+std::uniform_int_distribution
分别。 - 然后我使用“旧”方式生成了 960,000 个(可被 6 整除)随机数,并记录了数字 0-5 的频率。然后我计算了这些频率的标准偏差。我正在寻找的是尽可能低的标准偏差,因为如果分布真正均匀,就会发生这种情况。
- 我运行该模拟 1000 次并记录每个模拟的标准偏差。我还记录了以毫秒为单位的时间。
- 之后,我又做了同样的事情,但这次以“新”方式生成随机数。
- 最后,我计算了旧方法和新方法的标准差列表的平均值和标准差,以及旧方法和新方法所用时间列表的平均值和标准差。
结果如下:
[OLD WAY]
Spread
mean: 346.554406
std dev: 110.318361
Time Taken (ms)
mean: 6.662910
std dev: 0.366301
[NEW WAY]
Spread
mean: 350.346792
std dev: 110.449190
Time Taken (ms)
mean: 28.053907
std dev: 0.654964
令人惊讶的是,两种方法的卷的总分布是相同的。即, std::mt19937
+ std::uniform_int_distribution
不是比简单的“更统一” std::rand()
+ %
我所做的另一个观察是,新方法比旧方法慢了大约 4 倍。总的来说,我似乎在速度上付出了巨大的代价,而质量几乎没有提高。
我的实验在某些方面有缺陷吗?或者 std::rand()
真的没有那么糟糕,甚至可能更好?
作为参考,这是我完整使用的代码:
#include <cstdio>
#include <random>
#include <algorithm>
#include <chrono>
int getRandNum_Old() {
static bool init = false;
if (!init) {
std::srand(time(nullptr)); // Seed std::rand
init = true;
}
return std::rand() % 6;
}
int getRandNum_New() {
static bool init = false;
static std::random_device rd;
static std::mt19937 eng;
static std::uniform_int_distribution<int> dist(0,5);
if (!init) {
eng.seed(rd()); // Seed random engine
init = true;
}
return dist(eng);
}
template <typename T>
double mean(T* data, int n) {
double m = 0;
std::for_each(data, data+n, [&](T x){ m += x; });
m /= n;
return m;
}
template <typename T>
double stdDev(T* data, int n) {
double m = mean(data, n);
double sd = 0.0;
std::for_each(data, data+n, [&](T x){ sd += ((x-m) * (x-m)); });
sd /= n;
sd = sqrt(sd);
return sd;
}
int main() {
const int N = 960000; // Number of trials
const int M = 1000; // Number of simulations
const int D = 6; // Num sides on die
/* Do the things the "old" way (blech) */
int freqList_Old[D];
double stdDevList_Old[M];
double timeTakenList_Old[M];
for (int j = 0; j < M; j++) {
auto start = std::chrono::high_resolution_clock::now();
std::fill_n(freqList_Old, D, 0);
for (int i = 0; i < N; i++) {
int roll = getRandNum_Old();
freqList_Old[roll] += 1;
}
stdDevList_Old[j] = stdDev(freqList_Old, D);
auto end = std::chrono::high_resolution_clock::now();
auto dur = std::chrono::duration_cast<std::chrono::microseconds>(end-start);
double timeTaken = dur.count() / 1000.0;
timeTakenList_Old[j] = timeTaken;
}
/* Do the things the cool new way! */
int freqList_New[D];
double stdDevList_New[M];
double timeTakenList_New[M];
for (int j = 0; j < M; j++) {
auto start = std::chrono::high_resolution_clock::now();
std::fill_n(freqList_New, D, 0);
for (int i = 0; i < N; i++) {
int roll = getRandNum_New();
freqList_New[roll] += 1;
}
stdDevList_New[j] = stdDev(freqList_New, D);
auto end = std::chrono::high_resolution_clock::now();
auto dur = std::chrono::duration_cast<std::chrono::microseconds>(end-start);
double timeTaken = dur.count() / 1000.0;
timeTakenList_New[j] = timeTaken;
}
/* Display Results */
printf("[OLD WAY]\n");
printf("Spread\n");
printf(" mean: %.6f\n", mean(stdDevList_Old, M));
printf(" std dev: %.6f\n", stdDev(stdDevList_Old, M));
printf("Time Taken (ms)\n");
printf(" mean: %.6f\n", mean(timeTakenList_Old, M));
printf(" std dev: %.6f\n", stdDev(timeTakenList_Old, M));
printf("\n");
printf("[NEW WAY]\n");
printf("Spread\n");
printf(" mean: %.6f\n", mean(stdDevList_New, M));
printf(" std dev: %.6f\n", stdDev(stdDevList_New, M));
printf("Time Taken (ms)\n");
printf(" mean: %.6f\n", mean(timeTakenList_New, M));
printf(" std dev: %.6f\n", stdDev(timeTakenList_New, M));
}
原文由 rcplusplus 发布,翻译遵循 CC BY-SA 4.0 许可协议
几乎所有“旧”的实现
rand()
都使用 LCG ;虽然它们通常不是最好的生成器,但通常你不会看到它们在这样的基本测试中失败 - 即使是最差的 PRNG,平均偏差和标准偏差也通常是正确的。“坏”的常见故障 - 但足够常见 -
rand()
实现是:RAND_MAX
;尽管如此,这些都不是特定于
rand()
的 API。一个特定的实现可以在srand
/rand
后面放置一个 xorshift 系列生成器,并且从算法上讲,在不更改接口的情况下获得最先进的 PRNG,因此没有像这样的测试您确实会在输出中显示出任何弱点。编辑:@R。 correctly notes that the
rand
/srand
interface is limited by the fact thatsrand
takes anunsigned int
, so any generator an implementation may put它们后面本质上仅限于UINT_MAX
可能的起始种子(以及因此生成的序列)。确实如此,尽管 API 可以简单地扩展以使srand
采用unsigned long long
或添加单独的srand(unsigned char *, size_t)
过载。实际上,
rand()
的实际问题 原则上并没有太多的实现, 但是:向后兼容性;许多当前的实现使用次优生成器,通常使用错误选择的参数;一个臭名昭著的例子是 Visual C++,它的
RAND_MAX
只有 32767。然而,这不能轻易改变,因为它会破坏与过去的兼容性——人们使用srand
可重复模拟的种子不会太高兴(实际上,IIRC 上述实现可以追溯到 Microsoft C 早期版本 - 甚至是 Lattice C - 从八十年代中期开始);简单的界面;
rand()
为整个程序提供具有全局状态的单个生成器。虽然这对于许多简单的用例来说非常好(实际上非常方便),但它带来了问题:最后,
rand
状态:time(NULL)
不是,因为它不够精细,而且通常 - 认为没有 RTC 的嵌入式设备 - 甚至不够随机)。因此,新的
<random>
标头试图修复这种混乱,提供以下算法:…以及默认的
random_device
以及播种它们。现在,如果你问我,我 也 希望在此之上构建一个简单的 API,用于“简单”、“猜数字”的情况(类似于 Python 提供“复杂”API 的方式,但也很简单
random.randint
& Co. 为我们那些不想被随机设备/引擎/适配器/任何我们每次想为宾果卡提取数字时不被淹没在随机设备/引擎/适配器/任何东西中的简单的人使用全球预播 PRNG),但确实,您可以在当前设施上轻松地自己构建它(而在简单的设施上构建“完整”API 是不可能的)。最后,回到您的性能比较:正如其他人所指出的,您将快速 LCG 与较慢(但通常认为质量更好)的 Mersenne Twister 进行比较;如果您对 LCG 的质量没问题,您可以使用
std::minstd_rand
而不是std::mt19937
。事实上,在调整你的函数以使用
std::minstd_rand
并避免初始化时使用无用的静态变量我得到 9 毫秒(旧)和 21 毫秒(新);最后,如果我摆脱
dist
(与经典的模运算符相比,它处理输出范围的分布偏斜而不是输入范围的倍数)并回到你正在做的事情getRandNum_Old()
我把它降低到 6 毫秒(所以,快 30%),可能是因为,不像调用
rand()
,std::minstd_rand
更容易内联。顺便说一句,我使用手动滚动(但几乎符合标准库接口)
XorShift64*
进行了相同的测试,它比rand()
快 2.3 倍(3.68 ms vs 8.61 ms );鉴于这一点,与 Mersenne Twister 和 提供的各种 LCG 不同,它 通过了当前的随机测试套件, 而且速度非常快,这让你想知道为什么它还没有包含在标准库中。