在 Javascript 中为随机数生成器设定种子
是否可以在JavaScript中为随机数生成器(Math.random
)设定种子?
不,不可能 播种 。ECMAScript 规范在这个主题上有意含糊不清,没有提供种子设定的方法,也不要求浏览器甚至使用相同的算法。因此,这样的功能必须由外部提供,谢天谢地,这并不太难。Math.random()
我已经在普通的JavaScript中实现了许多好的,短的和快速的伪随机数生成器(PRNG)函数。所有这些都可以播种并提供高质量的数字。这些不是出于安全目的 - 如果您需要一个可播种的CSPRNG,请查看ISAAC。
首先,请注意正确初始化您的PRNG。为简单起见,下面的生成器没有内置的种子生成过程,但接受一个或多个 32 位数字作为 PRNG 的初始种子状态。相似或稀疏的种子(例如,简单种子 1 和 2)具有低熵,并且可能导致相关性或其他随机性质量问题,有时导致输出具有相似的属性(例如随机生成的水平相似)。为了避免这种情况,最佳做法是使用分布良好的高熵种子和/或超过前15个左右的数字来初始化PRNG。
有很多方法可以做到这一点,但这里有两种方法。首先,哈希函数非常擅长从短字符串生成种子。一个好的哈希函数即使两个字符串相似,也会生成非常不同的结果,所以你不必在字符串上花太多心思。下面是一个示例哈希函数:
function cyrb128(str) {
let h1 = 1779033703, h2 = 3144134277,
h3 = 1013904242, h4 = 2773480762;
for (let i = 0, k; i < str.length; i++) {
k = str.charCodeAt(i);
h1 = h2 ^ Math.imul(h1 ^ k, 597399067);
h2 = h3 ^ Math.imul(h2 ^ k, 2869860233);
h3 = h4 ^ Math.imul(h3 ^ k, 951274213);
h4 = h1 ^ Math.imul(h4 ^ k, 2716044179);
}
h1 = Math.imul(h3 ^ (h1 >>> 18), 597399067);
h2 = Math.imul(h4 ^ (h2 >>> 22), 2869860233);
h3 = Math.imul(h1 ^ (h3 >>> 17), 951274213);
h4 = Math.imul(h2 ^ (h4 >>> 19), 2716044179);
return [(h1^h2^h3^h4)>>>0, (h2^h1)>>>0, (h3^h1)>>>0, (h4^h1)>>>0];
}
调用将从字符串生成一个 128 位哈希值,该值可用于为 PRNG 设定种子。以下是使用它的方法:cyrb128
// Create cyrb128 state:
var seed = cyrb128("apples");
// Four 32-bit component hashes provide the seed for sfc32.
var rand = sfc32(seed[0], seed[1], seed[2], seed[3]);
// Only one 32-bit component hash is needed for mulberry32.
var rand = mulberry32(seed[0]);
// Obtain sequential random numbers like so:
rand();
rand();
注意:如果您想要一个稍微健壮的 128 位哈希,请考虑MurmurHash3_x86_128,它更彻底,但适用于大型数组。
或者,只需选择一些虚拟数据来填充种子,并事先将生成器推进几次(12-20次迭代)以彻底混合初始状态。这具有更简单的优点,并且经常用于PRNG的参考实现,但它确实限制了初始状态的数量:
var seed = 1337 ^ 0xDEADBEEF; // 32-bit seed with optional XOR value
// Pad seed with Phi, Pi and E.
// https://en.wikipedia.org/wiki/Nothing-up-my-sleeve_number
var rand = sfc32(0x9E3779B9, 0x243F6A88, 0xB7E15162, seed);
for (var i = 0; i < 15; i++) rand();
注意:这些PRNG函数的输出产生一个正的32位数(0到232-1),然后将其转换为0-1(0(包括0,1排除)之间的浮点数,等效于,如果您想要特定范围的随机数,请阅读MDN上的这篇文章。如果您只想要原始位,只需删除最终的除法操作即可。Math.random()
JavaScript 数字只能表示分辨率高达 53 位的整数。当使用按位运算时,这一数字减少到 32。其他语言的现代 PRNG 通常使用 64 位操作,这在移植到 JS 时需要填充码,这可能会大大降低性能。这里的算法仅使用32位操作,因为它与JS直接兼容。
sfc32是PractRand随机数测试套件的一部分(当然它通过了)。sfc32 具有 128 位状态,在 JS 中非常快。
function sfc32(a, b, c, d) {
return function() {
a >>>= 0; b >>>= 0; c >>>= 0; d >>>= 0;
var t = (a + b) | 0;
a = b ^ b >>> 9;
b = c + (c << 3) | 0;
c = (c << 21 | c >>> 11);
d = d + 1 | 0;
t = t + d | 0;
c = c + t | 0;
return (t >>> 0) / 4294967296;
}
}
您可能想知道 和 是做什么用的。这些本质上是 32 位整数转换,用于性能优化。 在JS中基本上是浮点数,但在按位操作期间,它们切换到32位整数模式。JS解释器处理此模式的速度更快,但任何乘法或加法都会导致它切换回浮点数,从而导致性能下降。| 0
>>>= 0
Number
Mulberry32是一个简单的生成器,具有32位状态,但速度非常快,并且具有良好的质量随机性(作者指出它通过了gjrand测试套件的所有测试,并且具有完整的232周期,但我还没有验证)。
function mulberry32(a) {
return function() {
var t = a += 0x6D2B79F5;
t = Math.imul(t ^ t >>> 15, t | 1);
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
return ((t ^ t >>> 14) >>> 0) / 4294967296;
}
}
如果你只需要一个简单但体面的PRNG并且不需要数十亿个随机数,我会推荐这个(参见生日问题)。
截至2018年5月,xoshiro128**是Vigna&Blackman的Xorshift家族的新成员(Vigna教授还负责Xorshift128 +算法,为引擎盖下的大多数实现提供支持)。它是提供128位状态的最快的生成器。Math.random
function xoshiro128ss(a, b, c, d) {
return function() {
var t = b << 9, r = a * 5; r = (r << 7 | r >>> 25) * 9;
c ^= a; d ^= b;
b ^= c; a ^= d; c ^= t;
d = d << 11 | d >>> 21;
return (r >>> 0) / 4294967296;
}
}
作者声称它很好地通过了随机性测试(尽管有警告)。其他研究人员指出,它在TestU01中未能通过一些测试(特别是LinearComp和BinaryRank)。在实践中,当使用浮点数时(例如在这些实现中),它不应该引起问题,但如果依赖于原始的最低阶位,则可能会导致问题。
这是鲍勃·詹金斯(Bob Jenkins,2007)的JSF或“smallprng”,他也制作了ISAAC和SpookyHash。它通过了PractRand测试,应该相当快,尽管没有sfc32那么快。
function jsf32(a, b, c, d) {
return function() {
a |= 0; b |= 0; c |= 0; d |= 0;
var t = a - (b << 27 | b >>> 5) | 0;
a = b ^ (c << 17 | c >>> 15);
b = c + d | 0;
c = d + t | 0;
d = a + t | 0;
return (d >>> 0) / 4294967296;
}
}