Javascript 中的随机数特征
在讨论这个随机数的问题之前,我们不妨先看一段简单的代码来看看我们使用的Math.random()方法生成随机数的效果是否真的具有随机的特征
- 首先定义了
getRandomNumber函数,用于生成指定范围内的随机数。 - 然后,
generateRandomRanges函数根据指定的范围、循环次数和区间数量,生成随机数并统计每个区间的出现次数。 - 最后,
printStatistics函数用于打印统计数据,显示每个区间的范围和对应的出现次数。
可以根据自己的需求修改代码中的参数,例如调整 min、max、n 和 m 的值,以及自定义打印统计数据的方式。
function getRandomNumber(min, max) {
// random 方法生成0到1之间的随机数
return Math.random() * (max - min) + min;
}
function generateRandomRanges(min, max, n, m) {
const rangeSize = (max - min) / m; // 每个区间的大小
const counts = Array(m).fill(0); // 初始化统计数组
for (let i = 0; i < n; i++) {
const randomNumber = getRandomNumber(min, max);
const rangeIndex = Math.floor((randomNumber - min) / rangeSize);
counts[rangeIndex]++;
}
return counts;
}
function printStatistics(counts, min, max, m) {
const rangeSize = (max - min) / m;
for (let i = 0; i < counts.length; i++) {
const rangeStart = min + i * rangeSize;
const rangeEnd = rangeStart + rangeSize;
const count = counts[i];
console.log(
`Range ${i}: [${rangeStart.toFixed(2)}, ${rangeEnd.toFixed(2)}): ${count}`
);
}
}
const min = 0; // 最小值
const max = 100; // 最大值
const n = 1e4; // 循环次数
const m = 10; // 区间数量
const counts = generateRandomRanges(min, max, n, m);
// [988, 974, 1042, 1024, 989, 1036, 985, 976, 995, 991]
printStatistics(counts, min, max, m);
可以看出随机数在大样本的基础下是均匀分布的,那么说明Math.randow() 真的是随机的吗?
让我们再看看落在各区间的增长趋势:
这个增长的势头非常有意思,仿佛随机数是1,2,3,4.... 这种均匀的增长,它真的随机了吗? 如随!
计算机中的随机数
事实上Math.random() 方法生成的是伪随机数,而不是真正的随机数。这是因为计算机程序是基于确定性算法的,无法真正产生完全随机的数字。
在背后,Math.random() 方法使用了一个种子(seed)来初始化一个伪随机数生成器(pseudo-random number generator,PRNG)。这个种子可以是当前的系统时间或程序指定的值。一旦种子确定,伪随机数生成器将按照确定性算法生成一个序列的数字。这个序列在统计学上表现得像是随机的,但实际上是可以被重现的。
因此,为了产生更加随机的数字,通常会使用更复杂的方法和更好的随机数生成器,如基于物理过程的随机性(如热噪声、量子现象等)或使用真正的硬件设备生成随机数。这样可以提高生成的数字的随机性和安全性。
总结起来,Math.random() 方法生成的是伪随机数,它是通过确定性算法生成的序列,看起来表现得像是随机的。要实现真正的随机性,可能需要采用更复杂的方法和更好的随机数生成器。
让我们在这个宏观视角来完成一个简单的随机数生成器:
function* pseudoRandomGenerator(seed: number = Date.now()): Generator<number, void, number> {
while (true) {
seed = (seed * 9301 + 49297) % 233280;
const random = seed / 233280;
yield random;
}
}
// 使用示例
const randomGen = pseudoRandomGenerator();
for (let i = 0; i < 10; i++) {
const randomNum = randomGen.next().value;
console.log(randomNum);
}
// 使用 part1的代码计算在1e5的样本容量下区间分布为:
// [9989, 10008, 9927, 9870, 9984, 10101, 10164, 10049, 9876, 10032]
在大多数编程语言中,Math.random() 方法的具体实现是由编程语言的库或运行时环境提供的,并且通常并不公开其具体的实现细节。因此,了解 Math.random() 背后的具体逻辑需要查看相关语言或库的文档或源代码。
然而,一般情况下,Math.random() 方法使用的是伪随机数生成器(PRNG),而不是真正的随机数生成器。伪随机数生成器使用确定性算法根据一个种子(seed)生成一个看似随机的序列。
那么看看我上面写的一个简单线性同余伪随机数生成器,其中9301和49297是常数,它们是经验性选择的,用于在生成伪随机数时控制其性质。这些常数的选择会影响最终生成的伪随机数的分布和周期性。
如果选择的常数不合适,可能会导致生成的伪随机数不均匀分布或具有较短的周期。因此,选择这些常数通常需要一些数学和实验分析来确保生成器产生的伪随机数满足预期的性质。
在这个特定示例中,9301和49297是经过测试和选择的常数,以便生成的伪随机数具有较好的均匀分布和适当的周期性。但是,这不是一个用于高度安全或加密目的的随机数生成器,因此不应该在对随机性要求非常高的场合使用。
常见的伪随机数生成器有多种算法,其中一种常用的是线性同余生成器(Linear Congruential Generator,LCG)。LCG 使用一个线性递推关系来生成伪随机数。它接受一个种子作为输入,并使用以下公式生成下一个伪随机数:
next = (previous * a + c) mod m
其中,previous 是上一个生成的伪随机数,a、c 和 m 是预先定义的常数。mod 表示取模运算。初始的种子值可以是系统时间、程序指定的值或其他值。
需要注意的是,具体的伪随机数生成器实现可能会有不同的参数和算法选择,因此在不同的编程语言或库中,Math.random() 的实现细节可能会有所不同。
如果你想深入了解特定编程语言中 Math.random() 的实现细节,我建议查阅相关的官方文档或源代码,以获得更具体和准确的信息。
那么接下来让我们大致看看javascript 在V8这个runtime中使用 Math.random()的底层逻辑吧!
V8 引擎中对伪随机数的实现
兴趣比较强烈可以通过标题跳转至源码实现处,速通可以直接看最后一段总结
在头文件 "math-random.h" 中,定义了一个名为 MathRandom 的类。该类是一个静态类 (AllStatic),用于处理 Math.random() 相关的操作。它包含以下成员函数:
-
InitializeContext(Isolate* isolate, Handle<Context> native_context):用于初始化 Math.random() 函数的上下文。它接受一个指向 Isolate 对象的指针和一个指向 Context 对象的句柄作为参数。在该函数中,它创建了一个固定大小的双精度浮点数数组(cache),并将其设置为上下文对象的属性。还创建了一个状态结构体(State)数组,并将其设置为上下文对象的属性。最后调用ResetContext()函数进行上下文的重置。 -
ResetContext(Tagged<Context> native_context):用于重置上下文对象的状态。它接受一个上下文对象的标记句柄作为参数。在该函数中,它将 Math.random() 的索引设置为零,并将状态结构体数组的第一个元素设置为零。 -
RefillCache(Isolate* isolate, Address raw_native_context):用于填充缓存。它接受一个指向 Isolate 对象的指针和一个原生上下文地址作为参数。在该函数中,它首先将原生上下文地址转换为上下文对象的标记句柄。然后,它获取状态结构体数组的第一个元素,并检查是否已经初始化。如果状态结构体的值为零,则根据随机种子生成新的状态。接下来,它获取上下文对象中的缓存数组,并使用 xorshift128+ 算法生成随机数,并将随机数存储到缓存数组中。最后,它更新 Math.random() 的索引,并返回新索引的指针。
在.cc 文件中,实现了在头文件中声明的 MathRandom 类的成员函数。具体实现如下:
-
InitializeContext(Isolate* isolate, Handle<Context> native_context):创建一个固定大小的双精度浮点数数组(cache),并将其设置为上下文对象的属性。然后创建一个状态结构体(State)数组,并将其设置为上下文对象的属性。最后调用ResetContext()函数进行上下文的重置。 -
ResetContext(Tagged<Context> native_context):将 Math.random() 的索引设置为零,并将状态结构体数组的第一个元素设置为零。 -
RefillCache(Isolate* isolate, Address raw_native_context):首先将原生上下文地址转换为上下文对象的标记句柄。然后获取状态结构体数组的第一个元素,并检查是否已经初始化。如果状态结构体的值为零,则根据随机种子生成新的状态。接下来,获取上下文对象中的缓存数组,并使用 xorshift128+ 算法生成随机数,并将随机数存储到缓存数组中。最后,更新 Math.random() 的索引,并返回新索引的指针。
总的来说,v8实现了 Math.random() 函数的上下文初始化、重置和缓存填充的功能。使用了 xorshift128+ 算法生成随机数,并将生成的随机数存储在一个固定大小的缓存数组中,以供 Math.random() 函数调用时使用。
xorshift128+
相信看到这里的小伙伴一定对xorshift128+非常好奇,简单来说它就是一种伪随机数生成算法
xorshift128+(也称为 XORShift128 Plus)是一种伪随机数生成算法,它是 xorshift 算法家族的一部分,通过执行位运算(XOR 和移位)操作在内部状态中生成伪随机数。它的名字来自于两个状态值之间的 XOR 操作和两个状态值的移位操作。
xorshift128+ 算法的关键思想是维护两个 64 位整数状态(s0 和 s1),并在每次生成随机数时,通过执行位运算操作来更新这两个状态值,然后将其中一个状态值转换为浮点数以生成随机数。算法的周期性较长,且随机数质量良好,适用于许多应用场景。
以下是 xorshift128+ 算法的伪代码示例:
state0 = initialSeed
state1 = anotherSeed
while true:
nextState0 = state0
nextState1 = state1
state0 = nextState1
nextState0 ^= nextState0 << 23
nextState0 ^= nextState0 >> 17
nextState0 ^= state1 ^ (state1 >> 26)
state1 = nextState0
yield (state1 + state0) / 2^64
在这个示例中,initialSeed 和 anotherSeed 是初始化的状态值。算法使用位运算操作,包括左移、右移和异或,来更新状态值,并生成伪随机数。最后,将状态值组合起来并除以 2^64 来生成一个位于 [0, 1) 之间的随机浮点数。
下面是一个使用 TypeScript 编写的简单示例,演示如何在 JavaScript/TypeScript 中实现 xorshift128+ 算法并生成随机数:
class Xorshift128Plus {
private state0: number;
private state1: number;
constructor(seed0: number, seed1: number) {
this.state0 = seed0;
this.state1 = seed1;
}
// Generate a random number between 0 and 1.
random(): number {
const nextState0 = this.state0;
const nextState1 = this.state1;
this.state0 = nextState1;
nextState0 ^= nextState0 << 23;
nextState0 ^= nextState0 >> 17;
nextState0 ^= nextState1 ^ (nextState1 >> 26);
this.state1 = nextState0;
return (nextState0 + nextState1) / 0x100000000; // 2^32
}
}
// Usage
const generator = new Xorshift128Plus(12345, 67890);
for (let i = 0; i < 10; i++) {
const randomNum = generator.random();
console.log(randomNum);
}
这个示例创建了一个 Xorshift128Plus 类,它接受两个种子值并提供 random 方法来生成伪随机数。然后,在 for 循环中生成了 10 个随机数并将它们打印到控制台上。
希望这篇文章能起到抛砖引玉的作用,在我们研发过程中加入更多的热情和探索精神,发现更多有意思的知识。