On Wed, Aug 13, 2025 at 12:06 AM Nathan Myers <n...@cantrip.org> wrote:

> Changes in v5:
>  * Static-assert movable RNG object correctly.
>  * Add a more comprehensive test gencanon.cc
>
> Changes in v4:
>  * Static-assert that arg is floating-point, coercible from bigger
> unsigned.
>  * Static-assert that arg satisfies uniform_random_bit_generator, movable.
>  * #include uniform_int_dist.h for concept uniform_random_bit_generator
>  * Coerce floating consts from unsigned literals, matching other usage.
>  * Remove redundant type decorations static and -> .
>  * Use __builtin_floor for broader applicability than std::floor.
>  * Use __builtin_pow in place of immediate lambda.
>  * Rename, rearrange local objects for clarity.
>  * Add redundant "if constexpr (k > 1)" for clarity.
>  * #if-guard new impl against Clang, which lacks constexpr floor() etc.
>
> Changes in v3:
>  * Implement exactly as specified in the WP, for C++26 and up.
>
> Implement P0952R2 "A new specification for std::generate_canonical".
> It has us start over if the naive generation of a float in 0..1
> comes up exactly equal to 1, which occurs too rarely for the change
> to measurably affect performance.
>
> A test for the case already appears in 64351.cc. This change amounts
> to a different resolution for PR64351 and LWG 2524. A new test,
> gencanon.cc, is more thorough.
>
> libstdc++-v3/ChangeLog:
>         PR libstdc++/119739
>         * include/bits/random.tcc: Add generate_canonical impl for C++26.
>         * include/std/random: Include uniform_int_dist.h for concept.
>         *
> testsuite/26_numerics/random/uniform_real_distribution/operators/64351.cc:
>         Adapt test for both pre- and post- C++26.
>         *
> testsuite/26_numerics/random/uniform_real_distribution/operators/gencanon.cc:
>         Test for float and double from 32-bit RNG, including 1.0 do-over.
> ---
>  libstdc++-v3/include/bits/random.tcc          | 48 +++++++++++
>  libstdc++-v3/include/std/random               |  1 +
>  .../operators/64351.cc                        |  8 +-
>  .../operators/gencanon.cc                     | 85 +++++++++++++++++++
>  4 files changed, 140 insertions(+), 2 deletions(-)
>  create mode 100644
> libstdc++-v3/testsuite/26_numerics/random/uniform_real_distribution/operators/gencanon.cc
>
> diff --git a/libstdc++-v3/include/bits/random.tcc
> b/libstdc++-v3/include/bits/random.tcc
> index 53ccacb2e38..65f457a6659 100644
> --- a/libstdc++-v3/include/bits/random.tcc
> +++ b/libstdc++-v3/include/bits/random.tcc
> @@ -3349,6 +3349,9 @@ namespace __detail
>         }
>      }
>
> +#if __cplusplus < 202400 || defined(__clang__)
>
I do not think that not implementing this issue for clang is really a good
option.
The function is small enough that you could test it on the compiler
explorer,
and you may use that to check how we behave for constant folding.
I would instead replace the constexpr variable with `const` and see if with
optimization GCC does constant fold calls to floor, pow etc.
Of as alternative, define a helper macro like
_GLIBCXX_GENCANONICAL_CONSTEXPR,
(and undefine it at end of the file),\ that would be constexpr for gcc and
empty for clang,
 and then define the variables as:
_GLIBCXX_GENCANONICAL_CONSTEXPR
const _RealT __Rk = __builtin_pow(__R, __k)

This would allow use to make them constexpr if later version of clang
supports that.

This is why I have also suggested adjusting the computation of __k, so we
can use __builting_log to compute the value faster in most common case.

> +  // Clang lacks constexpr __builtin_floor and __builtin_pow.
> +
>    template<typename _RealType, size_t __bits,
>            typename _UniformRandomNumberGenerator>
>      _RealType
> @@ -3385,6 +3388,51 @@ namespace __detail
>         }
>        return __ret;
>      }
> +#else
> +  template<typename _RealT, size_t __digits, typename _URBG>
> +    _RealT
> +    generate_canonical(_URBG& __urng)
> +    {
> +      static_assert(requires {
> +       [](uniform_random_bit_generator auto) {}(
> +           static_cast<std::remove_cv_t<_URBG>&&>(__urng)); },
> +       "argument must meet uniform_random_bit_generator requirements");
> +      static_assert(
> +       std::is_floating_point_v<_RealT> && requires {
> _RealT(_URBG::max()); },
> +       "template argument must be floating point, coercible from
> unsigned");
> +
> +      // Names are chosen to match the description in the Standard.
> +      const size_t __r = std::numeric_limits<_RealT>::radix;
> +      const auto __sample = [](auto __high) constexpr
> +       { return _RealT(__high - _URBG::min()); };
> +      constexpr _RealT __R = _RealT(1) + __sample(_URBG::max());
> +      const size_t __d_max = std::numeric_limits<_RealT>::digits;
> +      const size_t __d = __digits < __d_max ? __digits : __d_max;
> +      constexpr _RealT __rd = __builtin_pow(_RealT(__r), __d);
> +      const unsigned __k = [](_RealT __R, _RealT __rd) consteval
> +       {
> +         unsigned __i = 1;
> +         for (auto __Ri = __R; __Ri < __rd; __Ri *= __R)
> +           ++__i;
> +         return __i;
> +       } (__R, __rd);
> +      constexpr _RealT __Rk = __builtin_pow(__R, __k);
> +      constexpr _RealT __x = __builtin_floor(__Rk / __rd);
> +      constexpr _RealT __xrd = __x * __rd;
> +
> +      _RealT __sum;
> +      do
> +       {
> +         _RealT __Ri = _RealT(1);
> +         __sum = __sample(__urng());
> +         if constexpr (__k > 1)
> +           for (int __i = 1; __i != __k; ++__i)
> +             __Ri *= __R, __sum += __sample(__urng()) * __Ri;
> +       } while (__builtin_expect(__sum >= __xrd, false));
> +      _RealT __ret = __builtin_floor(__sum / __x) / __rd;
> +      return __ret;
> +    }
> +#endif
>
>  _GLIBCXX_END_NAMESPACE_VERSION
>  } // namespace
> diff --git a/libstdc++-v3/include/std/random
> b/libstdc++-v3/include/std/random
> index 0e058a04bd9..86d4f0a8380 100644
> --- a/libstdc++-v3/include/std/random
> +++ b/libstdc++-v3/include/std/random
> @@ -49,6 +49,7 @@
>  #include <type_traits>
>  #include <bits/random.h>
>  #include <bits/opt_random.h>
> +#include <bits/uniform_int_dist.h>
>  #include <bits/random.tcc>
>
>  #endif // C++11
> diff --git
> a/libstdc++-v3/testsuite/26_numerics/random/uniform_real_distribution/operators/64351.cc
> b/libstdc++-v3/testsuite/26_numerics/random/uniform_real_distribution/operators/64351.cc
> index 3c0cb8bbd49..6212a32a364 100644
> ---
> a/libstdc++-v3/testsuite/26_numerics/random/uniform_real_distribution/operators/64351.cc
> +++
> b/libstdc++-v3/testsuite/26_numerics/random/uniform_real_distribution/operators/64351.cc
> @@ -45,16 +45,20 @@ test02()
>    rng.seed(sequence);
>    rng.discard(12 * 629143);
>    std::mt19937 rng2{rng};
> +  int mismatch = 0;
>    for (int i = 0; i < 20; ++i)
>    {
>      float n =
>        std::generate_canonical<float,
> std::numeric_limits<float>::digits>(rng);
>      VERIFY( n != 1.f );
>
> -    // PR libstdc++/80137
>      rng2.discard(1);
> -    VERIFY( rng == rng2 );
>    }
> +  // PR libstdc++/80137
> +#if __cplusplus >= 202400
> +  rng2.discard(1);  // account for a 1.0 generated and discarded.
> +#endif
> +  VERIFY( rng == rng2 );
>  }
>
>  int
> diff --git
> a/libstdc++-v3/testsuite/26_numerics/random/uniform_real_distribution/operators/gencanon.cc
> b/libstdc++-v3/testsuite/26_numerics/random/uniform_real_distribution/operators/gencanon.cc
> new file mode 100644
> index 00000000000..ba781315e0d
> --- /dev/null
> +++
> b/libstdc++-v3/testsuite/26_numerics/random/uniform_real_distribution/operators/gencanon.cc
> @@ -0,0 +1,85 @@
> +// { dg-do run { target { c++26 && { ! simulator } } } }
> +
> +#include <random>
> +#include <limits>
> +#include <type_traits>
> +#include <cmath>
> +#include <testsuite_hooks.h>
> +
> +// Verify P0952R9 implementation requiring a second round-trip
> +// if first yields exactly 1. In this test, the RNG delivering
> +// 32 bits per call is seeded such that this occurs once on the
> +// sixth iteration for float, and not at all for double.
> +// However, each double iteration requires two calls to the RNG.
> +
> +template <typename T, typename RNG>
> +void
> +test01(RNG& rng, int& deviation, int& max, int& rms, int& zero, int&
> skips)
> +{
> +  const auto size = 1'000'000, buckets = 100;
> +  std::array<number, buckets> histo{};
> +  int zero = 0, skips = 0;
> +  for (auto i = 0; i != size; ++i) {
> +    T sample = std::generate_canonical<T, 100>(rng);
> +    VERIFY(sample >= 0.0);
> +    VERIFY(sample < 1.0)
> +    if (sample == 0.0) {
> +      ++zero;
> +    }
> +    auto bucket = static_cast<int>(std::floor(sample * buckets));
> +    ++histo[bucket];
> +    rng2.discard(1);
> +    if (rng != rng2) {
> +      ++skips;
> +      rng2.discard(1);
> +      VERIFY(rng == rng2)
> +    }
> +  }
> +  int devsquare = 0;
> +  for (int i = 0; i < buckets; ++i) {
> +    const auto expected = size / buckets;
> +    auto count = histo[i];
> +    auto diff = count - expected;
> +    if (diff < 0) diff = -diff;
> +    deviation += diff;
> +    devsquare += diff * diff;
> +    if (diff > max) max = diff;
> +  }
> +  rms = std::sqrt(devsquare);
> +}
> +
> +int
> +main()
> +{
> +  std::mt19937 rng(8890);
> +  std::seed_seq sequence{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
> +  rng.seed(sequence);
> +  rng.discard(12 * 629143);
> +
> +  {
> +    int deviation{}, max{}, rms{}, zero{}, skips{};
> +    auto rng2{rng};
> +    test01<float>(rng2, deviation, max, rms, zero, skips);
> +
> +    if (std::numeric_limits<float>::is_iec559) {
> +      VERIFY(deviation == 7032);
> +      VERIFY(max == 276);
> +      VERIFY(rms == 906);
> +      VERIFY(zero == 0);
> +    }
> +    VERIFY(skips == 1);
> +  }
> +  {
> +    auto rng3{rng}
> +    test01<double>(rng2, deviation, max, rms, zero, skips);
> +
> +    if (std::numeric_limits<double>::is_iec559) {
> +      VERIFY(deviation == 7650);
> +      VERIFY(max == 259);
> +      VERIFY(rms == 975);
> +      VERIFY(zero == 0);
> +    }
> +    VERIFY(skips == 1'000'000);
> +  }
> +  return 0;
> +}
> --
> 2.50.0
>
>

Reply via email to