Skip to content

Commit 34f0b2b

Browse files
sebhollfacebook-github-bot
authored andcommitted
Switch Math.random() to faster 64-bit seeded implementation. (#1175)
Summary: The `std::minstd_rand` implementation has its `seed()` value type defined as `result_type` which is aliased to `unsigned int`, a 32-bit value on supported platforms. Using 32-bit seeds and results can lead to real-world [birthday problem](https://en.wikipedia.org/wiki/Birthday_problem) collisions. As per Purujit's recommendation, we update the implementation of `Math.random()` to utilize a 64-bit LCG that can accept a 64-bit seed and also generate a 64-bit result. There is a lot of [analysis](https://prng.di.unimi.it/) and [blog posts](https://v8.dev/blog/math-random) around the best way to implement random number generators. This PR attempts to deliver two improvements on the current implementation: * Having 64-bit seeded values and result types instead of 32-bit. * Ideally, being faster or at least the same speed. * (optional) having better randomness properties. Addressing #1169, we benchmark the `xoroshiro128+` (used by browsers); `randomDevice` (cryptographically secure); `mt19937_64` (complex but fast) and `lcg64` (simple & fast) unsigned 64-bit integer PRNGs against three different uniform random distribution implementations: 1. **`std::uniform_real_distribution()`**: standard library implementation, but slow as it requires multiple random numbers to be generated for a single [0, 1) value; plus it has known bugs where it can in fact return 1 because of rounding. 2. **Bit Twiddle Approach 1**: use the `(0x3FFL << 52 | uint64_t(x) >>> 12) - 1.0` from Java and Firefox that pairs a fixed exponent with 52 random mantissa bits to generate a double between [1, 2) and then subtracting 1. 3. **Bit Twiddle Approach 2**: use the `(uint64_t(x) >> 11) * 0x1.0p-53` approach which generates a double between [0, 1) directly with 53 bits worth of possible output (twice as many values as the 52 bits from Approach 1). The bit twiddling approaches are described [here](https://prng.di.unimi.it/#:~:text=Generating%20uniform%20doubles%20in%20the%20unit%20interval). Given the large number of platforms and architectures that Hermes is compiled to, we make copious use of `static_assert`s to make sure that we don't encounter unexpected rug-pulls or changing bit-widths when upgrading compilers; or adding new targets. These should of course be elided after compilation and so will not affect runtime performance. ### Alternatives Considered - Instead of manually bit packing a 64-bit seed, an implementation leveraging C++11's `seed_seq` was explored that would ideally provide even more entropy, however the author of [this article](https://www.pcg-random.org/posts/cpp-seeding-surprises.html) asserts that 64 bits of seed seq data does not necessarily produce 64 bits of output. As such, we keep it simple with a single seed value so it is easier to reason with in the future. Pull Request resolved: #1175 Test Plan: We compile the following `benchmark.js` to byte-code using `../bin/hermes --emit-binary -fstatic-builtins -O ./benchmark.js -out ./benchmark.out.bin`: ``` const n_iterations = 10_000_000; let sum = 0, min = 2, max = -2; const rndFunction = Math.random; for (const i = 0; i < n_iterations; i++) { const rnd = rndFunction(); if (rnd < min) min = rnd; if (rnd > max) max = rnd; sum += rnd; } print(JSON.stringify({min, avg: sum / n_iterations, max})); ``` And then we execute this bytecode on each of the implementation's resulting `hvm` binary (I'm using `hyperfine`) on my local development machine (Apple MacBook Air M2 with ARM64): | Command | Mean [s] | Min [s] | Max [s] | Relative | |:---|---:|---:|---:|---:| | `./hvm-main ./benchmark.out.bin` | 8.371 ± 0.065 | 8.310 | 8.508 | 1.05 ± 0.01 | | `./hvm-xoroshiro128plus-twiddlebits ./benchmark.out.bin` | 8.075 ± 0.023 | 8.040 | 8.127 | 1.01 ± 0.00 | | **`./hvm-xoroshiro128plus-twiddlebits2 ./benchmark.out.bin`** | 8.019 ± 0.033 | 7.975 | 8.068 | 1.00 ± 0.01 | | `./hvm-lcg64-urd ./benchmark.out.bin` | 8.288 ± 0.041 | 8.232 | 8.369 | 1.04 ± 0.01 | | `./hvm-lcg64-twiddlebits ./benchmark.out.bin` | 7.988 ± 0.030 | 7.949 | 8.039 | 1.00 | | `./hvm-lcg64-twiddlebits2 ./benchmark.out.bin` | 8.010 ± 0.029 | 7.965 | 8.058 | 1.00 ± 0.01 | | `./hvm-mt19937_64-urd ./benchmark.out.bin` | 8.109 ± 0.047 | 8.058 | 8.190 | 1.02 ± 0.01 | | `./hvm-mt19937_64-twiddlebits ./benchmark.out.bin` | 8.135 ± 0.055 | 8.063 | 8.262 | 1.02 ± 0.01 | | `./hvm-mt19937_64-twiddlebits2 ./benchmark.out.bin` | 8.143 ± 0.039 | 8.067 | 8.187 | 1.02 ± 0.01 | | `./hvm-mt19937_64-urd-hoisted ./benchmark.out.bin` | 8.028 ± 0.029 | 7.977 | 8.082 | 1.01 ± 0.01 | | `./hvm-randomDevice-urd ./benchmark.out.bin` | 9.859 ± 0.036 | 9.800 | 9.918 | 1.23 ± 0.01 | | `./hvm-randomDevice-twiddlebits ./benchmark.out.bin` | 9.514 ± 0.028 | 9.463 | 9.550 | 1.19 ± 0.01 | | `./hvm-randomDevice-twiddlebits2 ./benchmark.out.bin` | 9.558 ± 0.033 | 9.498 | 9.610 | 1.20 ± 0.01 | Although `xoroshiro128plus-twiddlebits2` appears optimal, neildhar preferred `mt19937_64-urd` as it's less invasive a change and has less complexity to maintain. Reviewed By: avp Differential Revision: D50792073 Pulled By: neildhar fbshipit-source-id: 9a5a829dd32adf8986b912e4f424b48a6f937bd0
1 parent 5eac805 commit 34f0b2b

File tree

2 files changed

+14
-3
lines changed

2 files changed

+14
-3
lines changed

include/hermes/VM/JSLib/RuntimeCommonStorage.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ struct RuntimeCommonStorage {
3838
MockedEnvironment tracedEnv;
3939

4040
/// PRNG used by Math.random()
41-
std::minstd_rand randomEngine_;
41+
std::mt19937_64 randomEngine_;
4242
bool randomEngineSeeded_ = false;
4343
};
4444

lib/VM/JSLib/Math.cpp

+13-2
Original file line numberDiff line numberDiff line change
@@ -210,8 +210,19 @@ CallResult<HermesValue> mathPow(void *, Runtime &runtime, NativeArgs args) {
210210
CallResult<HermesValue> mathRandom(void *, Runtime &runtime, NativeArgs) {
211211
RuntimeCommonStorage *storage = runtime.getCommonStorage();
212212
if (!storage->randomEngineSeeded_) {
213-
std::minstd_rand::result_type seed;
214-
seed = std::random_device()();
213+
std::random_device randDevice;
214+
215+
auto randValue = randDevice();
216+
static_assert(
217+
sizeof(randValue) == 4, "expecting 32 bits from std::random_device()");
218+
219+
// Create a 64-bit seed using two 32-bit random numbers.
220+
uint64_t seed =
221+
(uint64_t(randValue) << (8 * sizeof(randValue))) | randDevice();
222+
static_assert(
223+
sizeof(decltype(storage->randomEngine_)::result_type) >= 8,
224+
"expecting at least 64-bit result_type for PRNG");
225+
215226
storage->randomEngine_.seed(seed);
216227
storage->randomEngineSeeded_ = true;
217228
}

0 commit comments

Comments
 (0)