是否可以在 JavaScript 中播种随机数生成器( Math.random
)?
原文由 weepy 发布,翻译遵循 CC BY-SA 4.0 许可协议
是否可以在 JavaScript 中播种随机数生成器( Math.random
)?
原文由 weepy 发布,翻译遵循 CC BY-SA 4.0 许可协议
不,不可能播种 Math.random()
。 ECMAScript 规范 在这个主题上故意含糊其辞,既不提供播种方式,也不要求浏览器使用相同的算法。所以必须要外部提供这样的功能,还好不是太难。
我已经在纯 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];
}
调用 cyrb128
将从一个字符串中生成一个 128 位哈希值,该值可用于为 PRNG 播种。以下是您可能如何使用它:
// 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 到 2 32 -1),然后将其转换为 0-1 之间的浮点数(0 包含,1 不包含)相当于 Math.random()
,如果您想要特定范围的随机数,请阅读 MDN 上的这篇文章。如果您只想要原始位,只需删除最后的除法运算即可。
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;
}
}
您可能想知道 | 0
和 >>>= 0
的用途。这些本质上是 32 位整数转换,用于性能优化。 Number
在JS中基本都是浮点数,但在按位运算时,会切换到32位整数模式。 JS 解释器处理此模式的速度更快,但任何乘法或加法都会导致它切换回浮点数,从而导致性能下降。
Mulberry32 是一个简单的 32 位状态生成器,但速度极快且具有良好的随机性(作者说它通过了 gjrand 测试套件的所有测试并且具有完整的 2 32周期,但我尚未验证)。
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 教授还负责为大多数 Math.random
实现提供支持的 Xorshift128+ 算法)。它是提供 128 位状态的最快生成器。
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;
}
}
原文由 bryc 发布,翻译遵循 CC BY-SA 4.0 许可协议
10 回答11.2k 阅读
5 回答4.8k 阅读✓ 已解决
4 回答3.1k 阅读✓ 已解决
2 回答2.7k 阅读✓ 已解决
3 回答2.3k 阅读✓ 已解决
3 回答2.2k 阅读✓ 已解决
2 回答2.6k 阅读✓ 已解决
不,不可能播种
Math.random()
,但是编写自己的生成器相当容易,或者更好的是,使用现有的生成器。退房: 这个相关的问题。
此外, 有关播种的更多信息,请参阅 David Bau 的博客。