Repository: microsoft/STL
Pull request #5412: `<random>`: Fast `discard()` for `minstd_rand` and `minstd_rand0` via fast exponentiation
Author: login: mirounga
Labels: name: performance
description: Must go faster
Created: 2025-04-17T13:48:41Z | Merged: 2025-04-22T22:59:51Z

discard() minstd_rand and minstd_rand0 now takes logarithmic time
due to fast exponentiation and avoiding constant divisions.




Diff:
diff --git a/benchmarks/src/random_integer_generation.cpp b/benchmarks/src/random_integer_generation.cpp
index 9c1615a5bc1..7a65f2b1f2e 100644
--- a/benchmarks/src/random_integer_generation.cpp
+++ b/benchmarks/src/random_integer_generation.cpp
@@ -31,6 +31,23 @@ void BM_lcg(benchmark::State& state) {
 }
 BENCHMARK(BM_lcg);
 
+/// Test discard()
+
+template <class Engine>
+void BM_discard(benchmark::State& state) {
+    Engine gen;
+    const auto n = static_cast<unsigned long long>(state.range(0));
+    for (auto _ : state) {
+        gen.discard(n);
+        benchmark::DoNotOptimize(gen());
+    }
+}
+BENCHMARK(BM_discard<std::mt19937>)->Range(0, 1 << 18);
+BENCHMARK(BM_discard<std::mt19937_64>)->Range(0, 1 << 18);
+BENCHMARK(BM_discard<std::minstd_rand>)->Range(0, 1 << 18);
+
+/// Support machinery for testing _Rng_from_urng and _Rng_from_urng_v2
+
 std::uint32_t GetMax() {
     std::mt19937 gen;
     std::uniform_int_distribution<std::uint32_t> dist(10'000'000, 20'000'000);
diff --git a/stl/inc/random b/stl/inc/random
index fa401b630cd..bc50e0c9f0e 100644
--- a/stl/inc/random
+++ b/stl/inc/random
@@ -617,12 +617,38 @@ public:
 
     void discard(unsigned long long _Nskip) noexcept /* strengthened */ {
         // discard _Nskip elements
-        auto _Temp = _Prev;
-        for (; 0 < _Nskip; --_Nskip) {
-            _Temp = _Next_linear_congruential_value<_Uint, _Ax, _Cx, _Mx>(_Temp);
-        }
+        if constexpr (_Cx == 0 && _Mx == 2147483647) {
+            // for minstd_rand and minstd_rand0 we can improve performance by
+            // performing fast exponentiation and avoiding constant divisions
+            auto _Temp = static_cast<unsigned long long>(_Prev);
+            auto _Mul  = static_cast<unsigned long long>(_Ax);
+
+            for (;;) {
+                if (_Nskip & 1) {
+                    _Temp = _Temp * _Mul;
+                    _Temp = (_Temp >> 31) + (_Temp & _Mx);
+                    _Temp = _Temp < _Mx ? _Temp : _Temp - _Mx;
+                }
 
-        _Prev = _Temp;
+                if (_Nskip >>= 1) {
+                    _Mul = _Mul * _Mul;
+                    _Mul = (_Mul >> 31) + (_Mul & _Mx);
+                    _Mul = _Mul < _Mx ? _Mul : _Mul - _Mx;
+                } else {
+                    break;
+                }
+            }
+
+            _Prev = static_cast<_Uint>(_Temp);
+        } else {
+            auto _Temp = _Prev;
+
+            for (; 0 < _Nskip; --_Nskip) {
+                _Temp = _Next_linear_congruential_value<_Uint, _Ax, _Cx, _Mx>(_Temp);
+            }
+
+            _Prev = _Temp;
+        }
     }
 
     _NODISCARD friend bool operator==(


Reviews:
user: login: StephanTLavavej
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-17T23:51:11Z
user: login: StephanTLavavej
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-17T23:52:13Z

Review comments:
diff_hunk: @@ -0,0 +1,18 @@
+2025-04-16T09:51:31-07:00
path: benchmark-random_integer_generation-results.csv
user: login: StephanTLavavej
body: This file should not have been added.
created_at: 2025-04-17T22:51:49Z
author_association: MEMBER
diff_hunk: @@ -617,12 +617,37 @@ public:
 
     void discard(unsigned long long _Nskip) noexcept /* strengthened */ {
         // discard _Nskip elements
-        auto _Temp = _Prev;
-        for (; 0 < _Nskip; --_Nskip) {
-            _Temp = _Next_linear_congruential_value<_Uint, _Ax, _Cx, _Mx>(_Temp);
-        }
+        if constexpr (_Cx == 0 && _Mx == 2147483647) {
+            // for minstd_rand and minstd_rand0 we can improve performance by
+            // performing fast exponentiation and avoiding constant divisions
+            auto _Temp = static_cast<unsigned long long>(_Prev);
+            auto _Mul  = static_cast<unsigned long long>(_Ax);
+
+            for (;;) {
+                if (_Nskip & 1) {
+                    _Temp = _Temp * _Mul;
+                    _Temp = (_Temp >> 31) + (_Temp & _Mx);
+                    _Temp = _Temp < _Mx ? _Temp : _Temp - _Mx;
+                }
+                if (_Nskip >>= 1) {
path: stl/inc/random
user: login: StephanTLavavej
body: Style: We conventionally add newlines between "non-chained" `if`-statements in order to make control flow clearer.
created_at: 2025-04-17T22:53:47Z
author_association: MEMBER
diff_hunk: @@ -617,12 +617,37 @@ public:
 
     void discard(unsigned long long _Nskip) noexcept /* strengthened */ {
         // discard _Nskip elements
-        auto _Temp = _Prev;
-        for (; 0 < _Nskip; --_Nskip) {
-            _Temp = _Next_linear_congruential_value<_Uint, _Ax, _Cx, _Mx>(_Temp);
-        }
+        if constexpr (_Cx == 0 && _Mx == 2147483647) {
+            // for minstd_rand and minstd_rand0 we can improve performance by
+            // performing fast exponentiation and avoiding constant divisions
+            auto _Temp = static_cast<unsigned long long>(_Prev);
+            auto _Mul  = static_cast<unsigned long long>(_Ax);
+
+            for (;;) {
+                if (_Nskip & 1) {
+                    _Temp = _Temp * _Mul;
+                    _Temp = (_Temp >> 31) + (_Temp & _Mx);
+                    _Temp = _Temp < _Mx ? _Temp : _Temp - _Mx;
+                }
+                if (_Nskip >>= 1) {
path: stl/inc/random
user: login: StephanTLavavej
body: No change requested: We generally avoid directly testing integers for non-zero-ness (except when working with bitmasks), and in modern code we generally avoid mixing side effects into conditions (although our older code did that quite often).

However, in this case, I am okay with the style as-is. Perhaps it's the near-symmetry of the `_Temp` and `_Mul` logic, but I feel that mechanically following our usual conventions would decrease clarity.
created_at: 2025-04-17T23:17:09Z
author_association: MEMBER
diff_hunk: @@ -617,12 +617,37 @@ public:
 
     void discard(unsigned long long _Nskip) noexcept /* strengthened */ {
         // discard _Nskip elements
-        auto _Temp = _Prev;
-        for (; 0 < _Nskip; --_Nskip) {
-            _Temp = _Next_linear_congruential_value<_Uint, _Ax, _Cx, _Mx>(_Temp);
-        }
+        if constexpr (_Cx == 0 && _Mx == 2147483647) {
+            // for minstd_rand and minstd_rand0 we can improve performance by
path: stl/inc/random
user: login: StephanTLavavej
body: Let's update the benchmark for this.
created_at: 2025-04-17T23:23:44Z
author_association: MEMBER

PR comments:
user: login: StephanTLavavej
created_at: 2025-04-17T23:53:40Z
body: Thanks! :heart_eyes_cat: I cleaned up a mistakenly added file, added a benchmark, and got the following hilarious results on my 5950X:

Benchmark                             |    Before | After   | Speedup
--------------------------------------|----------:|--------:|--------
`BM_discard<std::minstd_rand>/0`      |   1.68 ns | 1.69 ns | 0.99
`BM_discard<std::minstd_rand>/1`      |   3.32 ns | 2.93 ns | 1.13
`BM_discard<std::minstd_rand>/8`      |   14.8 ns | 5.99 ns | 2.47
`BM_discard<std::minstd_rand>/64`     |    107 ns | 9.52 ns | 11.24
`BM_discard<std::minstd_rand>/512`    |    843 ns | 16.6 ns | 50.78
`BM_discard<std::minstd_rand>/4096`   |   6805 ns | 19.0 ns | 358.16
`BM_discard<std::minstd_rand>/32768`  |  53885 ns | 24.6 ns | 2190.45
`BM_discard<std::minstd_rand>/262144` | 430735 ns | 29.1 ns | 14801.89

Skipping an exponential amount of work is pretty awesome.
author_association: MEMBER
user: login: StephanTLavavej
created_at: 2025-04-22T10:15:33Z
body: I'm mirroring this to the MSVC-internal repo - please notify me if any further changes are pushed.
author_association: MEMBER
user: login: StephanTLavavej
created_at: 2025-04-22T23:00:28Z
body: Thanks for perhaps the biggest speedup in the STL ever! :stopwatch: :heart_eyes_cat: :cat:
author_association: MEMBER