On Wed, 22 Jul 2026 at 15:53 +0200, Tomasz Kamiński wrote:
Since r11-3757-g98c37d3bacbb2f the uniform_int_distribution uses Lemire's
algorithm for engines outputting power of two range. As this algorithm
(in-contrast to other) rejects low outputs from incoming generator, this
change lead to hangs (due infinite loop) when distribution was mixed
with non-uniform generator, that returned 0 constantly. Such usage has
undefined behavior according to standard, however didn't lead to hangs
prior GCC 11 or (reportedly) standard libraries.
"prior to GCC 11 or with other standard libraries."
This patch, addresses above by changing the (slower) rejection path of the
algorithm, to alternate between rejecting low/high value of the range. The
output remains uniform, as only number of values rejected from span is
important.
Strictly speaking, it's not only the number of values that are
rejected, it has to be correctly distributed values. For example,
given a 2-bit URBG and range 3 we have product=x*3 and so for x in
[0,3] we get these {low,high} pairs:
{0,0}, {3,0}, {2,1}, {1,2}
We need to reject *one* of the pairs with high=0 because that way our
outputs will be 0,1,2 with equal probability. But it's important that
we reject either {0,0} or {3,0}, not {2,1} or {1,2}. So it's not
*only* the number of values that get rejected that matters.
Maybe say something like "The output remains uniform, as the same
number of values are rejected and the distribution of high-word values
is preserved even though different low-word values are rejected".
I don't have a proof that the distribution is preserved, but it's true
for the examples I've worked through with pencil and paper.
In consequence, of above the algorithm finishes successfully
for any generator that returns same value constantly.
While, it is possible to create an generator that would still lead to
infinite loop, by alternating between low and high values, I believe
this is inherent property on any algorithm that rejects some of the
engine's outputs.
PR libstdc++/118665
libstdc++-v3/ChangeLog:
* include/bits/uniform_int_dist.h
(uniform_int_distribution::_S_nd): Alternate between rejecting
from beginning/end of the range.
*
testsuite/26_numerics/random/uniform_int_distribution/operators/pr118665.cc:
New test.
---
I have benchmarked this locally, measuring time required to generate
1000 values from distribution. OLD is before the change, NEW is after:
-- NEW
--------------------------------------------------------
Benchmark Time CPU Iterations
--------------------------------------------------------
BM_mt19937 2778 ns 2762 ns 241094
BM_philox4x32 4175 ns 4151 ns 183153
BM_Constant 343 ns 342 ns 2009755
-- OLD
--------------------------------------------------------
Benchmark Time CPU Iterations
--------------------------------------------------------
BM_mt19937 2982 ns 2970 ns 233294
BM_philox4x32 4181 ns 4167 ns 167653
BM_Constant 337 ns 336 ns 2070099
The Constant is engine that returns a value of volatile
variable (otherwise it was optimized to constant by compiler).
Tested onx x86_64 linux locally. Additionally *random* test passed
in all standard modes, debug, -m32. OK for trunk?
Should we backport ths (after letting it bake for sometime) up to 13?
This was reported as regression. It will impact the result observed
from distribution, but not as much as introducing Lemiere's algoritghm
did.
libstdc++-v3/include/bits/uniform_int_dist.h | 9 ++++++
.../operators/pr118665.cc | 28 +++++++++++++++++++
2 files changed, 37 insertions(+)
create mode 100644
libstdc++-v3/testsuite/26_numerics/random/uniform_int_distribution/operators/pr118665.cc
diff --git a/libstdc++-v3/include/bits/uniform_int_dist.h
b/libstdc++-v3/include/bits/uniform_int_dist.h
index 9c5514c7c13..8ffce793482 100644
--- a/libstdc++-v3/include/bits/uniform_int_dist.h
+++ b/libstdc++-v3/include/bits/uniform_int_dist.h
@@ -273,6 +273,15 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
_Up __threshold = -__range % __range;
while (__low < __threshold)
{
+ // Algorithm is modified to alternate between rejecting from
+ // begining (__low) and end (__high) of the range. This
+ // guarantee stop for non-uniform engines that always results
With fixed spelling+grammar:
// The algorithm is modified to alternate between rejecting
// from the beginning and end of the range. This guarantees
// that we stop for non-uniform engines that always result
// in values below the threshold.
I removed the mentions of (__low) and (__high) since those names
do not refer to where in the range the value occurs (see below).
+ // in __low value.
+ __product = _Wp(__g()) * _Wp(__range);
+ _Up __high = _Up(__product);
The name __high is misleading, this is taking the low N/2 bits of
__product, not the high N/2 bits. It's exactly the same as the
assignment to __low further down. Should we just reuse __low here?
I don't think we need a second local variable.
i.e. I don't think the name "low" refers to rejecting values at the
low end of the range, I think it refers to the low word from
__product.
+ if (__high <= _Up_traits::__max - __threshold)
I know we currently only use Lemire for uint32 and uint64, but if we
used it with _Up == uint16_t then __max - __threshold would promote to
signed int, and we could get a -Wsign-compare warning here.
Could you use <= _Up(_Up_traits::__max - __threshold) instead. Or
declare it where we declare __threshold above:
_Up __upper_threshold = _Up_traits::__max - __threshold;
+ break;
+
__product = _Wp(__g()) * _Wp(__range);
__low = _Up(__product);
}
diff --git
a/libstdc++-v3/testsuite/26_numerics/random/uniform_int_distribution/operators/pr118665.cc
b/libstdc++-v3/testsuite/26_numerics/random/uniform_int_distribution/operators/pr118665.cc
new file mode 100644
index 00000000000..979d5fdfca9
--- /dev/null
+++
b/libstdc++-v3/testsuite/26_numerics/random/uniform_int_distribution/operators/pr118665.cc
@@ -0,0 +1,28 @@
+// { dg-do run { target c++11 } }
+// { dg-require-cstdint "" }
+
+#include <random>
+#include <testsuite_hooks.h>
+
+class Constant {
+public:
+ using result_type = unsigned;
+ static constexpr result_type min() { return 0u; }
+ static constexpr result_type max() { return ~0u; }
+
+ // always return 0
+ result_type operator()() {
+ ++calls;
+ VERIFY( calls < 1000 );
+ return 0;
+ }
+
+ unsigned calls = 0;
+};
+
+int main()
+{
+ Constant gen;
+ std::uniform_int_distribution<int> dist(0, 42);
+ dist(gen);
+}
--
2.55.0