Skip to content

Commit 20c11c4

Browse files
avpfacebook-github-bot
authored andcommitted
Switch Math.random() to faster 64-bit seeded implementation. (#1175) (#1175)
Summary: Original Author: [email protected] Original Git: 34f0b2b Original Reviewed By: avp Original Revision: D50792073 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 Pulled By: neildhar Reviewed By: neildhar Differential Revision: D50992797 fbshipit-source-id: 0926d1dda08b217bdc46b0731a03b4cd3cb39812
1 parent 1a349dc commit 20c11c4

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
@@ -205,8 +205,19 @@ CallResult<HermesValue> mathPow(void *, Runtime &runtime, NativeArgs args) {
205205
CallResult<HermesValue> mathRandom(void *, Runtime &runtime, NativeArgs) {
206206
RuntimeCommonStorage *storage = runtime.getCommonStorage();
207207
if (!storage->randomEngineSeeded_) {
208-
std::minstd_rand::result_type seed;
209-
seed = std::random_device()();
208+
std::random_device randDevice;
209+
210+
auto randValue = randDevice();
211+
static_assert(
212+
sizeof(randValue) == 4, "expecting 32 bits from std::random_device()");
213+
214+
// Create a 64-bit seed using two 32-bit random numbers.
215+
uint64_t seed =
216+
(uint64_t(randValue) << (8 * sizeof(randValue))) | randDevice();
217+
static_assert(
218+
sizeof(decltype(storage->randomEngine_)::result_type) >= 8,
219+
"expecting at least 64-bit result_type for PRNG");
220+
210221
storage->randomEngine_.seed(seed);
211222
storage->randomEngineSeeded_ = true;
212223
}

0 commit comments

Comments
 (0)