为何VC++编译器会导致这种统计分布模式异常?
首先先把你的测试代码贴出来方便参考:
#include <iostream> #include <vector> #include <cmath> #include <cstdlib> #include <chrono> using namespace std; const int N = 200; // Number of tests. const int M = 2000000; // Number of pseudo-random values generated per test. const int VALS = 2; // Number of possible values (values from 0 to VALS-1). const int ESP = M / VALS; // Expected number of appearances of each value per test. int main() { for (int i = 0; i < N; ++i) { unsigned seed = chrono::system_clock::now().time_since_epoch().count(); srand(seed); vector<int> hist(VALS, 0); for (int j = 0; j < M; ++j) ++hist[rand() % VALS]; int Y = 0; for (int j = 0; j < VALS; ++j) Y += abs(hist[j] - ESP); cout << Y << endl; } }
先简化下程序逻辑:当VALS=2时,你其实是在统计M次rand()%2生成的0和1的数量,然后计算Y=2*|0的数量 - M/2|(因为0和1的总数是M,两者的偏差绝对值相等)。所以Y的分布本质上反映的是rand()%2生成序列的奇偶分布偏差规律。
接下来拆解两个编译器的核心差异:
1. VC++的rand()是分布交替变化的根源
VC++的标准库rand()用的是一个特定的线性同余生成器(LCG),核心公式为:next = (current * 214013 + 2531011) % 2^31
之后返回这个值的高16位(即next >> 16),所以返回范围是0到32767。
当你执行rand()%2时,本质是取返回值的最低位。问题在于:这个LCG生成的序列,低位的周期性和相关性极强——简单说,生成的奇偶序列不是完全随机的,而是带有规律的波动。当M取不同值时,这种周期性会被放大:
- 如果M刚好是低位周期的整数倍或半整数倍,生成的0和1的数量会出现极端偏差(要么偏多要么偏少),导致Y的分布呈现双峰、类均匀的情况(极端值占比更高);
- 如果M刚好能“平均”掉这种周期性,偏差就会趋近正态分布,Y也就呈现正态分布。
这就是你看到VC++下Y的分布随M增大交替变化的原因。
2. GCC/Clang的rand()更符合随机特性
GCC和Clang采用的是glibc实现的rand(),同样是LCG,但参数不同:next = (current * 1103515245 + 12345) % 2^31
它返回的是0到2^31-1的完整范围,虽然rand()%2还是取最低位,但这个LCG的低位序列随机性要好得多,相关性极低。
当M很大时,M次rand()%2的结果近似服从二项分布,而二项分布在大样本下会趋近正态分布。Y是偏差绝对值的两倍,所以自然呈现半正态分布——这就是你看到GCC/Clang下Y始终保持半正态分布的原因。
总结
本质就是不同编译器的rand()底层实现差异:
- VC++的
rand()低位序列规律性太强,导致不同M值下奇偶分布偏差呈现交替的极端/正常模式; - GCC/Clang的
rand()低位随机性更好,偏差符合大样本下的正态特性,所以Y始终是半正态分布。
内容的提问来源于stack exchange,提问作者Manelicus

