From d1744fa3bf18de9cd22be9dbf79ec0fa75510571 Mon Sep 17 00:00:00 2001 From: Francisco Geiman Thiesen Date: Tue, 2 Dec 2025 13:23:36 -0800 Subject: [PATCH 01/17] Implement batched random integer generation for shuffle Implements the algorithm from Brackett-Rozinsky & Lemire's paper "Batched Ranged Random Integer Generation" to reduce RNG calls in std::shuffle and ranges::shuffle for 64-bit URNGs like mt19937_64. The batched approach extracts multiple bounded random integers from a single 64-bit random word, using only multiplication (no division) in the common case. This reduces the number of RNG calls by approximately half for arrays with fewer than 2^32 elements. Resolves #5736 --- stl/inc/algorithm | 267 +++++++++++++++++- .../tests/P0896R4_ranges_alg_shuffle/test.cpp | 120 ++++++++ 2 files changed, 379 insertions(+), 8 deletions(-) diff --git a/stl/inc/algorithm b/stl/inc/algorithm index e95528c127d..260a2a95f02 100644 --- a/stl/inc/algorithm +++ b/stl/inc/algorithm @@ -6096,6 +6096,92 @@ namespace ranges { #endif // _HAS_CXX20 #endif // _HAS_CXX17 +// Batched random integer generation for shuffle optimization. +// From Nevin Brackett-Rozinsky and Daniel Lemire, "Batched Ranged Random Integer Generation", +// Software: Practice and Experience 55(1), 2025. +// +// This algorithm extracts multiple bounded random integers from a single 64-bit random word, +// using only multiplication (no division) in the common case. + +template +struct _Batched_rng_from_urng { + // Batched random generation is only beneficial for 64-bit RNGs with full range. + // It requires the RNG to produce values in [0, 2^64 - 1]. + using _Urng_result = _Invoke_result_t<_Urng&>; + + static constexpr bool _Has_full_64bit_range = sizeof(_Urng_result) >= sizeof(uint64_t) + && is_unsigned_v<_Urng_result> && (_Urng::min) () == 0 + && (_Urng::max) () == (numeric_limits::max) (); + + // Threshold bounds for batch sizes based on array size. + // These are derived from the paper to minimize expected cost per random value. + // Batch size k requires product of k consecutive bounds <= 2^64. + static constexpr uint64_t _Bound_for_batch_6 = 21; // (21)^6 = 85,766,121 < 2^64 / (very conservative) + static constexpr uint64_t _Bound_for_batch_5 = 73; // (73)^5 = 2,073,071,593 < 2^64 + static constexpr uint64_t _Bound_for_batch_4 = 302; // (302)^4 = 8,319,430,096 < 2^64 + static constexpr uint64_t _Bound_for_batch_3 = 2642; // (2642)^3 = 18,454,249,288 < 2^64 + static constexpr uint64_t _Bound_for_batch_2 = 4294967296; // 2^32, for batch of 2 + + _Urng& _Ref; + + explicit _Batched_rng_from_urng(_Urng& _Func) noexcept : _Ref(_Func) {} + + // Generate a single bounded random value in [0, _Bound) using Lemire's method. + _NODISCARD _Diff _Single_bounded(_Diff _Bound) { + _Unsigned128 _Product{_Base128::_UMul128(static_cast(_Ref()), static_cast(_Bound), + _Product._Word[1])}; + auto _Leftover = _Product._Word[0]; + + if (_Leftover < static_cast(_Bound)) { + const uint64_t _Threshold = (0 - static_cast(_Bound)) % static_cast(_Bound); + while (_Leftover < _Threshold) { + _Product = _Unsigned128{_Base128::_UMul128( + static_cast(_Ref()), static_cast(_Bound), _Product._Word[1])}; + _Leftover = _Product._Word[0]; + } + } + + return static_cast<_Diff>(_Product._Word[1]); + } + + // Generate two bounded random values from a single 64-bit random word. + // The bounds are (n+1) and n for Fisher-Yates shuffle positions _Target_index and _Target_index-1. + void _Batch_2(_Diff* _Results, _Diff _Bound1, _Diff _Bound2) { + const uint64_t _B1 = static_cast(_Bound1); + const uint64_t _B2 = static_cast(_Bound2); + const uint64_t _Product_bound = _B1 * _B2; + + uint64_t _Random_word = static_cast(_Ref()); + + _Unsigned128 _Prod1{_Base128::_UMul128(_Random_word, _B1, _Prod1._Word[1])}; + _Results[0] = static_cast<_Diff>(_Prod1._Word[1]); + uint64_t _Leftover1 = _Prod1._Word[0]; + + _Unsigned128 _Prod2{_Base128::_UMul128(_Leftover1, _B2, _Prod2._Word[1])}; + _Results[1] = static_cast<_Diff>(_Prod2._Word[1]); + uint64_t _Leftover = _Prod2._Word[0]; + + // Rejection sampling: check if leftover is below threshold. + if (_Leftover < _Product_bound) { + const uint64_t _Threshold = (0 - _Product_bound) % _Product_bound; + while (_Leftover < _Threshold) { + _Random_word = static_cast(_Ref()); + + _Prod1 = _Unsigned128{_Base128::_UMul128(_Random_word, _B1, _Prod1._Word[1])}; + _Results[0] = static_cast<_Diff>(_Prod1._Word[1]); + _Leftover1 = _Prod1._Word[0]; + + _Prod2 = _Unsigned128{_Base128::_UMul128(_Leftover1, _B2, _Prod2._Word[1])}; + _Results[1] = static_cast<_Diff>(_Prod2._Word[1]); + _Leftover = _Prod2._Word[0]; + } + } + } + + _Batched_rng_from_urng(const _Batched_rng_from_urng&) = delete; + _Batched_rng_from_urng& operator=(const _Batched_rng_from_urng&) = delete; +}; + template class _Rng_from_urng_v2 { // wrap a URNG as an RNG public: @@ -6439,11 +6525,91 @@ void _Random_shuffle1(_RanIt _First, _RanIt _Last, _RngFn& _RngFunc) { } } +// Batched shuffle implementation for 64-bit URNGs with full range. +// Uses batched random generation to reduce RNG calls. +template +void _Random_shuffle_batched(_RanIt _First, _RanIt _Last, _Urng& _Func) { + // shuffle [_First, _Last) using batched random generation + _STD _Adl_verify_range(_First, _Last); + auto _UFirst = _STD _Get_unwrapped(_First); + const auto _ULast = _STD _Get_unwrapped(_Last); + if (_UFirst == _ULast) { + return; + } + + using _Diff = _Iter_diff_t<_RanIt>; + _Batched_rng_from_urng<_Diff, _Urng> _BatchedRng(_Func); + + auto _UTarget = _UFirst; + _Diff _Target_index = 1; + + // Process pairs using batched generation when beneficial. + // Batch of 2 is beneficial when bounds fit in 32 bits (product fits in 64 bits). + while (_UTarget != _ULast) { + ++_UTarget; + if (_UTarget == _ULast) { + break; + } + + const _Diff _Bound1 = _Target_index + 1; // bound for current position + const _Diff _Bound2 = _Target_index + 2; // bound for next position + + // Check if we can batch: both bounds and their product must fit safely. + // Use batch of 2 when the larger bound is <= 2^32 (product fits in 64 bits). + if (static_cast(_Bound2) <= _Batched_rng_from_urng<_Diff, _Urng>::_Bound_for_batch_2) { + auto _UTarget_next = _UTarget; + ++_UTarget_next; + + if (_UTarget_next != _ULast) { + // Generate two random indices in one batch. + _Diff _Offsets[2]; + _BatchedRng._Batch_2(_Offsets, _Bound1, _Bound2); + + _STL_ASSERT(0 <= _Offsets[0] && _Offsets[0] <= _Target_index, "random value out of range"); + _STL_ASSERT(0 <= _Offsets[1] && _Offsets[1] <= _Target_index + 1, "random value out of range"); + + // Perform first swap. + if (_Offsets[0] != _Target_index) { + swap(*_UTarget, *(_UFirst + _Offsets[0])); // intentional ADL + } + + // Advance to next position and perform second swap. + ++_UTarget; + ++_Target_index; + + if (_Offsets[1] != _Target_index) { + swap(*_UTarget, *(_UFirst + _Offsets[1])); // intentional ADL + } + + ++_UTarget; + ++_Target_index; + continue; + } + } + + // Fall back to single generation for this position. + const _Diff _Off = _BatchedRng._Single_bounded(_Bound1); + _STL_ASSERT(0 <= _Off && _Off <= _Target_index, "random value out of range"); + if (_Off != _Target_index) { + swap(*_UTarget, *(_UFirst + _Off)); // intentional ADL + } + + ++_UTarget; + ++_Target_index; + } +} + _EXPORT_STD template void shuffle(_RanIt _First, _RanIt _Last, _Urng&& _Func) { // shuffle [_First, _Last) using URNG _Func using _Urng0 = remove_reference_t<_Urng>; - _Rng_from_urng_v2<_Iter_diff_t<_RanIt>, _Urng0> _RngFunc(_Func); - _STD _Random_shuffle1(_First, _Last, _RngFunc); + + // Use batched shuffle when the URNG produces full 64-bit range values. + if constexpr (_Batched_rng_from_urng<_Iter_diff_t<_RanIt>, _Urng0>::_Has_full_64bit_range) { + _STD _Random_shuffle_batched(_First, _Last, _Func); + } else { + _Rng_from_urng_v2<_Iter_diff_t<_RanIt>, _Urng0> _RngFunc(_Func); + _STD _Random_shuffle1(_First, _Last, _RngFunc); + } } #if _HAS_CXX20 @@ -6455,20 +6621,37 @@ namespace ranges { _STATIC_CALL_OPERATOR _It operator()(_It _First, _Se _Last, _Urng&& _Func) _CONST_CALL_OPERATOR { _STD _Adl_verify_range(_First, _Last); - _Rng_from_urng_v2, remove_reference_t<_Urng>> _RngFunc(_Func); - auto _UResult = _Shuffle_unchecked( - _RANGES _Unwrap_iter<_Se>(_STD move(_First)), _RANGES _Unwrap_sent<_It>(_STD move(_Last)), _RngFunc); + using _Urng0 = remove_reference_t<_Urng>; + using _Diff = iter_difference_t<_It>; - _STD _Seek_wrapped(_First, _STD move(_UResult)); + // Use batched shuffle when the URNG produces full 64-bit range values. + if constexpr (_Batched_rng_from_urng<_Diff, _Urng0>::_Has_full_64bit_range) { + auto _UResult = _Shuffle_unchecked_batched( + _RANGES _Unwrap_iter<_Se>(_STD move(_First)), _RANGES _Unwrap_sent<_It>(_STD move(_Last)), _Func); + _STD _Seek_wrapped(_First, _STD move(_UResult)); + } else { + _Rng_from_urng_v2<_Diff, _Urng0> _RngFunc(_Func); + auto _UResult = _Shuffle_unchecked( + _RANGES _Unwrap_iter<_Se>(_STD move(_First)), _RANGES _Unwrap_sent<_It>(_STD move(_Last)), _RngFunc); + _STD _Seek_wrapped(_First, _STD move(_UResult)); + } return _First; } template requires permutable> && uniform_random_bit_generator> _STATIC_CALL_OPERATOR borrowed_iterator_t<_Rng> operator()(_Rng&& _Range, _Urng&& _Func) _CONST_CALL_OPERATOR { - _Rng_from_urng_v2, remove_reference_t<_Urng>> _RngFunc(_Func); + using _Urng0 = remove_reference_t<_Urng>; + using _Diff = range_difference_t<_Rng>; - return _RANGES _Rewrap_iterator(_Range, _Shuffle_unchecked(_Ubegin(_Range), _Uend(_Range), _RngFunc)); + // Use batched shuffle when the URNG produces full 64-bit range values. + if constexpr (_Batched_rng_from_urng<_Diff, _Urng0>::_Has_full_64bit_range) { + return _RANGES _Rewrap_iterator( + _Range, _Shuffle_unchecked_batched(_Ubegin(_Range), _Uend(_Range), _Func)); + } else { + _Rng_from_urng_v2<_Diff, _Urng0> _RngFunc(_Func); + return _RANGES _Rewrap_iterator(_Range, _Shuffle_unchecked(_Ubegin(_Range), _Uend(_Range), _RngFunc)); + } } private: @@ -6496,6 +6679,74 @@ namespace ranges { } return _Target; } + + // Batched shuffle implementation for ranges. + template + _NODISCARD static _It _Shuffle_unchecked_batched(_It _First, const _Se _Last, _Urng& _Func) { + // shuffle [_First, _Last) using batched random generation + _STL_INTERNAL_STATIC_ASSERT(random_access_iterator<_It>); + _STL_INTERNAL_STATIC_ASSERT(sentinel_for<_Se, _It>); + _STL_INTERNAL_STATIC_ASSERT(permutable<_It>); + + if (_First == _Last) { + return _First; + } + + using _Diff = iter_difference_t<_It>; + _Batched_rng_from_urng<_Diff, _Urng> _BatchedRng(_Func); + + auto _Target = _First; + _Diff _Target_index = 1; + + // Process pairs using batched generation when beneficial. + while (_Target != _Last) { + ++_Target; + if (_Target == _Last) { + break; + } + + const _Diff _Bound1 = _Target_index + 1; + const _Diff _Bound2 = _Target_index + 2; + + if (static_cast(_Bound2) <= _Batched_rng_from_urng<_Diff, _Urng>::_Bound_for_batch_2) { + auto _Target_next = _Target; + ++_Target_next; + + if (_Target_next != _Last) { + _Diff _Offsets[2]; + _BatchedRng._Batch_2(_Offsets, _Bound1, _Bound2); + + _STL_ASSERT(0 <= _Offsets[0] && _Offsets[0] <= _Target_index, "random value out of range"); + _STL_ASSERT(0 <= _Offsets[1] && _Offsets[1] <= _Target_index + 1, "random value out of range"); + + if (_Offsets[0] != _Target_index) { + _RANGES iter_swap(_Target, _First + _Offsets[0]); + } + + ++_Target; + ++_Target_index; + + if (_Offsets[1] != _Target_index) { + _RANGES iter_swap(_Target, _First + _Offsets[1]); + } + + ++_Target; + ++_Target_index; + continue; + } + } + + const _Diff _Off = _BatchedRng._Single_bounded(_Bound1); + _STL_ASSERT(0 <= _Off && _Off <= _Target_index, "random value out of range"); + if (_Off != _Target_index) { + _RANGES iter_swap(_Target, _First + _Off); + } + + ++_Target; + ++_Target_index; + } + return _Target; + } }; _EXPORT_STD inline constexpr _Shuffle_fn shuffle; diff --git a/tests/std/tests/P0896R4_ranges_alg_shuffle/test.cpp b/tests/std/tests/P0896R4_ranges_alg_shuffle/test.cpp index 6c2bb00b8a3..18fd2f7fd45 100644 --- a/tests/std/tests/P0896R4_ranges_alg_shuffle/test.cpp +++ b/tests/std/tests/P0896R4_ranges_alg_shuffle/test.cpp @@ -6,15 +6,18 @@ #include #include #include +#include #include #include #include +#include #include using namespace std; const unsigned int seed = random_device{}(); mt19937 gen{seed}; +mt19937_64 gen64{seed}; // 64-bit generator for batched random path // Validate dangling story static_assert(same_as{}, gen)), ranges::dangling>); @@ -72,8 +75,125 @@ void test_urbg() { // COMPILE-ONLY ranges::shuffle(arr, RandGen{}); } +// Test that shuffle produces a valid permutation for various sizes. +// This exercises both the batched path (for 64-bit RNGs) and the fallback path. +void test_shuffle_permutation() { + // Test with 64-bit generator (batched random path) + { + vector v(100); + iota(v.begin(), v.end(), 0); + vector original = v; + + shuffle(v.begin(), v.end(), gen64); + + // Verify it's still a permutation + vector sorted_v = v; + sort(sorted_v.begin(), sorted_v.end()); + assert(sorted_v == original); + } + + // Test with ranges::shuffle and 64-bit generator + { + vector v(100); + iota(v.begin(), v.end(), 0); + vector original = v; + + ranges::shuffle(v, gen64); + + // Verify it's still a permutation + vector sorted_v = v; + sort(sorted_v.begin(), sorted_v.end()); + assert(sorted_v == original); + } + + // Test with 32-bit generator (non-batched path) + { + vector v(100); + iota(v.begin(), v.end(), 0); + vector original = v; + + shuffle(v.begin(), v.end(), gen); + + // Verify it's still a permutation + vector sorted_v = v; + sort(sorted_v.begin(), sorted_v.end()); + assert(sorted_v == original); + } + + // Test with ranges::shuffle and 32-bit generator + { + vector v(100); + iota(v.begin(), v.end(), 0); + vector original = v; + + ranges::shuffle(v, gen); + + // Verify it's still a permutation + vector sorted_v = v; + sort(sorted_v.begin(), sorted_v.end()); + assert(sorted_v == original); + } +} + +// Test edge cases for shuffle +void test_shuffle_edge_cases() { + // Empty range + { + vector v; + shuffle(v.begin(), v.end(), gen64); + assert(v.empty()); + } + + // Single element + { + vector v = {42}; + shuffle(v.begin(), v.end(), gen64); + assert(v.size() == 1); + assert(v[0] == 42); + } + + // Two elements + { + vector v = {1, 2}; + vector original = v; + shuffle(v.begin(), v.end(), gen64); + sort(v.begin(), v.end()); + assert(v == original); + } + + // Three elements (odd count, tests batching boundary) + { + vector v = {1, 2, 3}; + vector original = v; + shuffle(v.begin(), v.end(), gen64); + sort(v.begin(), v.end()); + assert(v == original); + } + + // Four elements (even count) + { + vector v = {1, 2, 3, 4}; + vector original = v; + shuffle(v.begin(), v.end(), gen64); + sort(v.begin(), v.end()); + assert(v == original); + } + + // Large array to ensure batching is effective + { + vector v(10000); + iota(v.begin(), v.end(), 0); + vector original = v; + shuffle(v.begin(), v.end(), gen64); + sort(v.begin(), v.end()); + assert(v == original); + } +} + int main() { printf("Using seed: %u\n", seed); test_random(); + test_shuffle_permutation(); + test_shuffle_edge_cases(); } From 3df88561ae112ac3f4a64d699d600e7bbfacde0d Mon Sep 17 00:00:00 2001 From: Francisco Geiman Thiesen Date: Tue, 2 Dec 2025 19:19:17 -0800 Subject: [PATCH 02/17] Address review feedback: extract URNG range check to variable template Move _Has_full_64bit_range check out of _Batched_rng_from_urng class to reduce template instantiation overhead. Use is_same_v and _Max_limit as suggested by reviewer. --- stl/inc/algorithm | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/stl/inc/algorithm b/stl/inc/algorithm index 260a2a95f02..c5b92f1d2cf 100644 --- a/stl/inc/algorithm +++ b/stl/inc/algorithm @@ -6103,15 +6103,14 @@ namespace ranges { // This algorithm extracts multiple bounded random integers from a single 64-bit random word, // using only multiplication (no division) in the common case. +// Check if a URNG has full 64-bit range [0, 2^64 - 1]. +// Batched random generation is only beneficial for such RNGs. +template +constexpr bool _Urng_has_full_64bit_range = + is_same_v<_Invoke_result_t<_Urng&>, uint64_t> && (_Urng::min)() == 0 && (_Urng::max)() == _Max_limit(); + template struct _Batched_rng_from_urng { - // Batched random generation is only beneficial for 64-bit RNGs with full range. - // It requires the RNG to produce values in [0, 2^64 - 1]. - using _Urng_result = _Invoke_result_t<_Urng&>; - - static constexpr bool _Has_full_64bit_range = sizeof(_Urng_result) >= sizeof(uint64_t) - && is_unsigned_v<_Urng_result> && (_Urng::min) () == 0 - && (_Urng::max) () == (numeric_limits::max) (); // Threshold bounds for batch sizes based on array size. // These are derived from the paper to minimize expected cost per random value. @@ -6604,7 +6603,7 @@ void shuffle(_RanIt _First, _RanIt _Last, _Urng&& _Func) { // shuffle [_First, _ using _Urng0 = remove_reference_t<_Urng>; // Use batched shuffle when the URNG produces full 64-bit range values. - if constexpr (_Batched_rng_from_urng<_Iter_diff_t<_RanIt>, _Urng0>::_Has_full_64bit_range) { + if constexpr (_Urng_has_full_64bit_range<_Urng0>) { _STD _Random_shuffle_batched(_First, _Last, _Func); } else { _Rng_from_urng_v2<_Iter_diff_t<_RanIt>, _Urng0> _RngFunc(_Func); @@ -6625,7 +6624,7 @@ namespace ranges { using _Diff = iter_difference_t<_It>; // Use batched shuffle when the URNG produces full 64-bit range values. - if constexpr (_Batched_rng_from_urng<_Diff, _Urng0>::_Has_full_64bit_range) { + if constexpr (_Urng_has_full_64bit_range<_Urng0>) { auto _UResult = _Shuffle_unchecked_batched( _RANGES _Unwrap_iter<_Se>(_STD move(_First)), _RANGES _Unwrap_sent<_It>(_STD move(_Last)), _Func); _STD _Seek_wrapped(_First, _STD move(_UResult)); @@ -6645,7 +6644,7 @@ namespace ranges { using _Diff = range_difference_t<_Rng>; // Use batched shuffle when the URNG produces full 64-bit range values. - if constexpr (_Batched_rng_from_urng<_Diff, _Urng0>::_Has_full_64bit_range) { + if constexpr (_Urng_has_full_64bit_range<_Urng0>) { return _RANGES _Rewrap_iterator( _Range, _Shuffle_unchecked_batched(_Ubegin(_Range), _Uend(_Range), _Func)); } else { From 8aa184ab394fc54f3f15bdb05bcb79200de9d2dd Mon Sep 17 00:00:00 2001 From: Francisco Geiman Thiesen Date: Tue, 2 Dec 2025 19:49:01 -0800 Subject: [PATCH 03/17] Apply clang-format --- stl/inc/algorithm | 16 ++++++++-------- .../tests/P0896R4_ranges_alg_shuffle/test.cpp | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/stl/inc/algorithm b/stl/inc/algorithm index c5b92f1d2cf..a19e4332658 100644 --- a/stl/inc/algorithm +++ b/stl/inc/algorithm @@ -6107,7 +6107,7 @@ namespace ranges { // Batched random generation is only beneficial for such RNGs. template constexpr bool _Urng_has_full_64bit_range = - is_same_v<_Invoke_result_t<_Urng&>, uint64_t> && (_Urng::min)() == 0 && (_Urng::max)() == _Max_limit(); + is_same_v<_Invoke_result_t<_Urng&>, uint64_t> && (_Urng::min) () == 0 && (_Urng::max) () == _Max_limit(); template struct _Batched_rng_from_urng { @@ -6127,14 +6127,14 @@ struct _Batched_rng_from_urng { // Generate a single bounded random value in [0, _Bound) using Lemire's method. _NODISCARD _Diff _Single_bounded(_Diff _Bound) { - _Unsigned128 _Product{_Base128::_UMul128(static_cast(_Ref()), static_cast(_Bound), - _Product._Word[1])}; + _Unsigned128 _Product{ + _Base128::_UMul128(static_cast(_Ref()), static_cast(_Bound), _Product._Word[1])}; auto _Leftover = _Product._Word[0]; if (_Leftover < static_cast(_Bound)) { const uint64_t _Threshold = (0 - static_cast(_Bound)) % static_cast(_Bound); while (_Leftover < _Threshold) { - _Product = _Unsigned128{_Base128::_UMul128( + _Product = _Unsigned128{_Base128::_UMul128( static_cast(_Ref()), static_cast(_Bound), _Product._Word[1])}; _Leftover = _Product._Word[0]; } @@ -6146,8 +6146,8 @@ struct _Batched_rng_from_urng { // Generate two bounded random values from a single 64-bit random word. // The bounds are (n+1) and n for Fisher-Yates shuffle positions _Target_index and _Target_index-1. void _Batch_2(_Diff* _Results, _Diff _Bound1, _Diff _Bound2) { - const uint64_t _B1 = static_cast(_Bound1); - const uint64_t _B2 = static_cast(_Bound2); + const uint64_t _B1 = static_cast(_Bound1); + const uint64_t _B2 = static_cast(_Bound2); const uint64_t _Product_bound = _B1 * _B2; uint64_t _Random_word = static_cast(_Ref()); @@ -6630,8 +6630,8 @@ namespace ranges { _STD _Seek_wrapped(_First, _STD move(_UResult)); } else { _Rng_from_urng_v2<_Diff, _Urng0> _RngFunc(_Func); - auto _UResult = _Shuffle_unchecked( - _RANGES _Unwrap_iter<_Se>(_STD move(_First)), _RANGES _Unwrap_sent<_It>(_STD move(_Last)), _RngFunc); + auto _UResult = _Shuffle_unchecked(_RANGES _Unwrap_iter<_Se>(_STD move(_First)), + _RANGES _Unwrap_sent<_It>(_STD move(_Last)), _RngFunc); _STD _Seek_wrapped(_First, _STD move(_UResult)); } return _First; diff --git a/tests/std/tests/P0896R4_ranges_alg_shuffle/test.cpp b/tests/std/tests/P0896R4_ranges_alg_shuffle/test.cpp index 18fd2f7fd45..0d8082be24f 100644 --- a/tests/std/tests/P0896R4_ranges_alg_shuffle/test.cpp +++ b/tests/std/tests/P0896R4_ranges_alg_shuffle/test.cpp @@ -154,7 +154,7 @@ void test_shuffle_edge_cases() { // Two elements { - vector v = {1, 2}; + vector v = {1, 2}; vector original = v; shuffle(v.begin(), v.end(), gen64); sort(v.begin(), v.end()); @@ -163,7 +163,7 @@ void test_shuffle_edge_cases() { // Three elements (odd count, tests batching boundary) { - vector v = {1, 2, 3}; + vector v = {1, 2, 3}; vector original = v; shuffle(v.begin(), v.end(), gen64); sort(v.begin(), v.end()); @@ -172,7 +172,7 @@ void test_shuffle_edge_cases() { // Four elements (even count) { - vector v = {1, 2, 3, 4}; + vector v = {1, 2, 3, 4}; vector original = v; shuffle(v.begin(), v.end(), gen64); sort(v.begin(), v.end()); From b838edc6e80b6edafbfb4ec20e72bf2d11c52c64 Mon Sep 17 00:00:00 2001 From: "Stephan T. Lavavej" Date: Sat, 21 Feb 2026 05:21:57 -0800 Subject: [PATCH 04/17] Naming: `_B1` => `_Bx1`, `_B2` => `_Bx2` --- stl/inc/algorithm | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/stl/inc/algorithm b/stl/inc/algorithm index 8fed1fea08b..3c0a5de6f80 100644 --- a/stl/inc/algorithm +++ b/stl/inc/algorithm @@ -6228,17 +6228,17 @@ struct _Batched_rng_from_urng { // Generate two bounded random values from a single 64-bit random word. // The bounds are (n+1) and n for Fisher-Yates shuffle positions _Target_index and _Target_index-1. void _Batch_2(_Diff* _Results, _Diff _Bound1, _Diff _Bound2) { - const uint64_t _B1 = static_cast(_Bound1); - const uint64_t _B2 = static_cast(_Bound2); - const uint64_t _Product_bound = _B1 * _B2; + const uint64_t _Bx1 = static_cast(_Bound1); + const uint64_t _Bx2 = static_cast(_Bound2); + const uint64_t _Product_bound = _Bx1 * _Bx2; uint64_t _Random_word = static_cast(_Ref()); - _Unsigned128 _Prod1{_Base128::_UMul128(_Random_word, _B1, _Prod1._Word[1])}; + _Unsigned128 _Prod1{_Base128::_UMul128(_Random_word, _Bx1, _Prod1._Word[1])}; _Results[0] = static_cast<_Diff>(_Prod1._Word[1]); uint64_t _Leftover1 = _Prod1._Word[0]; - _Unsigned128 _Prod2{_Base128::_UMul128(_Leftover1, _B2, _Prod2._Word[1])}; + _Unsigned128 _Prod2{_Base128::_UMul128(_Leftover1, _Bx2, _Prod2._Word[1])}; _Results[1] = static_cast<_Diff>(_Prod2._Word[1]); uint64_t _Leftover = _Prod2._Word[0]; @@ -6248,11 +6248,11 @@ struct _Batched_rng_from_urng { while (_Leftover < _Threshold) { _Random_word = static_cast(_Ref()); - _Prod1 = _Unsigned128{_Base128::_UMul128(_Random_word, _B1, _Prod1._Word[1])}; + _Prod1 = _Unsigned128{_Base128::_UMul128(_Random_word, _Bx1, _Prod1._Word[1])}; _Results[0] = static_cast<_Diff>(_Prod1._Word[1]); _Leftover1 = _Prod1._Word[0]; - _Prod2 = _Unsigned128{_Base128::_UMul128(_Leftover1, _B2, _Prod2._Word[1])}; + _Prod2 = _Unsigned128{_Base128::_UMul128(_Leftover1, _Bx2, _Prod2._Word[1])}; _Results[1] = static_cast<_Diff>(_Prod2._Word[1]); _Leftover = _Prod2._Word[0]; } From 3091d9d0d91709bc00f4ccb05a53ddee3025886d Mon Sep 17 00:00:00 2001 From: "Stephan T. Lavavej" Date: Sat, 21 Feb 2026 05:27:48 -0800 Subject: [PATCH 05/17] Scope `_Diff` more tightly. --- stl/inc/algorithm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stl/inc/algorithm b/stl/inc/algorithm index 3c0a5de6f80..83729b0ddd9 100644 --- a/stl/inc/algorithm +++ b/stl/inc/algorithm @@ -6703,7 +6703,6 @@ namespace ranges { _STD _Adl_verify_range(_First, _Last); using _Urng0 = remove_reference_t<_Urng>; - using _Diff = iter_difference_t<_It>; // Use batched shuffle when the URNG produces full 64-bit range values. if constexpr (_Urng_has_full_64bit_range<_Urng0>) { @@ -6711,6 +6710,7 @@ namespace ranges { _RANGES _Unwrap_iter<_Se>(_STD move(_First)), _RANGES _Unwrap_sent<_It>(_STD move(_Last)), _Func); _STD _Seek_wrapped(_First, _STD move(_UResult)); } else { + using _Diff = iter_difference_t<_It>; _Rng_from_urng_v2<_Diff, _Urng0> _RngFunc(_Func); auto _UResult = _Shuffle_unchecked(_RANGES _Unwrap_iter<_Se>(_STD move(_First)), _RANGES _Unwrap_sent<_It>(_STD move(_Last)), _RngFunc); @@ -6723,13 +6723,13 @@ namespace ranges { requires permutable> && uniform_random_bit_generator> _STATIC_CALL_OPERATOR borrowed_iterator_t<_Rng> operator()(_Rng&& _Range, _Urng&& _Func) _CONST_CALL_OPERATOR { using _Urng0 = remove_reference_t<_Urng>; - using _Diff = range_difference_t<_Rng>; // Use batched shuffle when the URNG produces full 64-bit range values. if constexpr (_Urng_has_full_64bit_range<_Urng0>) { return _RANGES _Rewrap_iterator( _Range, _Shuffle_unchecked_batched(_Ubegin(_Range), _Uend(_Range), _Func)); } else { + using _Diff = range_difference_t<_Rng>; _Rng_from_urng_v2<_Diff, _Urng0> _RngFunc(_Func); return _RANGES _Rewrap_iterator(_Range, _Shuffle_unchecked(_Ubegin(_Range), _Uend(_Range), _RngFunc)); } From 9c639532b5b84d46ba07525aebb91569baa872dd Mon Sep 17 00:00:00 2001 From: "Stephan T. Lavavej" Date: Sat, 21 Feb 2026 05:33:01 -0800 Subject: [PATCH 06/17] Add `const` to `_UFirst`. --- stl/inc/algorithm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stl/inc/algorithm b/stl/inc/algorithm index 83729b0ddd9..2e269dcbb2d 100644 --- a/stl/inc/algorithm +++ b/stl/inc/algorithm @@ -6612,8 +6612,8 @@ template void _Random_shuffle_batched(_RanIt _First, _RanIt _Last, _Urng& _Func) { // shuffle [_First, _Last) using batched random generation _STD _Adl_verify_range(_First, _Last); - auto _UFirst = _STD _Get_unwrapped(_First); - const auto _ULast = _STD _Get_unwrapped(_Last); + const auto _UFirst = _STD _Get_unwrapped(_First); + const auto _ULast = _STD _Get_unwrapped(_Last); if (_UFirst == _ULast) { return; } From bbad415c1a009d581eef8b32874a2f08f8c284c6 Mon Sep 17 00:00:00 2001 From: "Stephan T. Lavavej" Date: Sat, 21 Feb 2026 05:49:26 -0800 Subject: [PATCH 07/17] Drop unnecessary `static_cast` for `_Ref()`, add internal static assertion. --- stl/inc/algorithm | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/stl/inc/algorithm b/stl/inc/algorithm index 2e269dcbb2d..b20eaad9878 100644 --- a/stl/inc/algorithm +++ b/stl/inc/algorithm @@ -6193,6 +6193,7 @@ constexpr bool _Urng_has_full_64bit_range = template struct _Batched_rng_from_urng { + _STL_INTERNAL_STATIC_ASSERT(_Urng_has_full_64bit_range<_Urng>); // Threshold bounds for batch sizes based on array size. // These are derived from the paper to minimize expected cost per random value. @@ -6209,15 +6210,13 @@ struct _Batched_rng_from_urng { // Generate a single bounded random value in [0, _Bound) using Lemire's method. _NODISCARD _Diff _Single_bounded(_Diff _Bound) { - _Unsigned128 _Product{ - _Base128::_UMul128(static_cast(_Ref()), static_cast(_Bound), _Product._Word[1])}; + _Unsigned128 _Product{_Base128::_UMul128(_Ref(), static_cast(_Bound), _Product._Word[1])}; auto _Leftover = _Product._Word[0]; if (_Leftover < static_cast(_Bound)) { const uint64_t _Threshold = (0 - static_cast(_Bound)) % static_cast(_Bound); while (_Leftover < _Threshold) { - _Product = _Unsigned128{_Base128::_UMul128( - static_cast(_Ref()), static_cast(_Bound), _Product._Word[1])}; + _Product = _Unsigned128{_Base128::_UMul128(_Ref(), static_cast(_Bound), _Product._Word[1])}; _Leftover = _Product._Word[0]; } } @@ -6232,7 +6231,7 @@ struct _Batched_rng_from_urng { const uint64_t _Bx2 = static_cast(_Bound2); const uint64_t _Product_bound = _Bx1 * _Bx2; - uint64_t _Random_word = static_cast(_Ref()); + uint64_t _Random_word = _Ref(); _Unsigned128 _Prod1{_Base128::_UMul128(_Random_word, _Bx1, _Prod1._Word[1])}; _Results[0] = static_cast<_Diff>(_Prod1._Word[1]); @@ -6246,7 +6245,7 @@ struct _Batched_rng_from_urng { if (_Leftover < _Product_bound) { const uint64_t _Threshold = (0 - _Product_bound) % _Product_bound; while (_Leftover < _Threshold) { - _Random_word = static_cast(_Ref()); + _Random_word = _Ref(); _Prod1 = _Unsigned128{_Base128::_UMul128(_Random_word, _Bx1, _Prod1._Word[1])}; _Results[0] = static_cast<_Diff>(_Prod1._Word[1]); From 592335d65a3d84d673a9cae0804f924a1bfb11f3 Mon Sep 17 00:00:00 2001 From: "Stephan T. Lavavej" Date: Sat, 21 Feb 2026 05:56:20 -0800 Subject: [PATCH 08/17] Add `const` to `_Bound`, `_Bound1`, `_Bound2` params. --- stl/inc/algorithm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stl/inc/algorithm b/stl/inc/algorithm index b20eaad9878..2a07030b313 100644 --- a/stl/inc/algorithm +++ b/stl/inc/algorithm @@ -6209,7 +6209,7 @@ struct _Batched_rng_from_urng { explicit _Batched_rng_from_urng(_Urng& _Func) noexcept : _Ref(_Func) {} // Generate a single bounded random value in [0, _Bound) using Lemire's method. - _NODISCARD _Diff _Single_bounded(_Diff _Bound) { + _NODISCARD _Diff _Single_bounded(const _Diff _Bound) { _Unsigned128 _Product{_Base128::_UMul128(_Ref(), static_cast(_Bound), _Product._Word[1])}; auto _Leftover = _Product._Word[0]; @@ -6226,7 +6226,7 @@ struct _Batched_rng_from_urng { // Generate two bounded random values from a single 64-bit random word. // The bounds are (n+1) and n for Fisher-Yates shuffle positions _Target_index and _Target_index-1. - void _Batch_2(_Diff* _Results, _Diff _Bound1, _Diff _Bound2) { + void _Batch_2(_Diff* _Results, const _Diff _Bound1, const _Diff _Bound2) { const uint64_t _Bx1 = static_cast(_Bound1); const uint64_t _Bx2 = static_cast(_Bound2); const uint64_t _Product_bound = _Bx1 * _Bx2; From d9c6d6b52959f89e1e4706f626fe434431c23c1e Mon Sep 17 00:00:00 2001 From: "Stephan T. Lavavej" Date: Sat, 21 Feb 2026 05:59:17 -0800 Subject: [PATCH 09/17] `_Batch_2()` should take a reference-to-array. --- stl/inc/algorithm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stl/inc/algorithm b/stl/inc/algorithm index 2a07030b313..5836d4b0e9c 100644 --- a/stl/inc/algorithm +++ b/stl/inc/algorithm @@ -6226,7 +6226,7 @@ struct _Batched_rng_from_urng { // Generate two bounded random values from a single 64-bit random word. // The bounds are (n+1) and n for Fisher-Yates shuffle positions _Target_index and _Target_index-1. - void _Batch_2(_Diff* _Results, const _Diff _Bound1, const _Diff _Bound2) { + void _Batch_2(_Diff (&_Results)[2], const _Diff _Bound1, const _Diff _Bound2) { const uint64_t _Bx1 = static_cast(_Bound1); const uint64_t _Bx2 = static_cast(_Bound2); const uint64_t _Product_bound = _Bx1 * _Bx2; From 0b9e0ce1c90758eeb364245b077aef5cc38d3cce Mon Sep 17 00:00:00 2001 From: "Stephan T. Lavavej" Date: Sat, 21 Feb 2026 06:01:46 -0800 Subject: [PATCH 10/17] Extract `static_cast(_Bound)` as `_Bx`. --- stl/inc/algorithm | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/stl/inc/algorithm b/stl/inc/algorithm index 5836d4b0e9c..48bf8c9701a 100644 --- a/stl/inc/algorithm +++ b/stl/inc/algorithm @@ -6210,13 +6210,14 @@ struct _Batched_rng_from_urng { // Generate a single bounded random value in [0, _Bound) using Lemire's method. _NODISCARD _Diff _Single_bounded(const _Diff _Bound) { - _Unsigned128 _Product{_Base128::_UMul128(_Ref(), static_cast(_Bound), _Product._Word[1])}; + const uint64_t _Bx = static_cast(_Bound); + _Unsigned128 _Product{_Base128::_UMul128(_Ref(), _Bx, _Product._Word[1])}; auto _Leftover = _Product._Word[0]; - if (_Leftover < static_cast(_Bound)) { - const uint64_t _Threshold = (0 - static_cast(_Bound)) % static_cast(_Bound); + if (_Leftover < _Bx) { + const uint64_t _Threshold = (0 - _Bx) % _Bx; while (_Leftover < _Threshold) { - _Product = _Unsigned128{_Base128::_UMul128(_Ref(), static_cast(_Bound), _Product._Word[1])}; + _Product = _Unsigned128{_Base128::_UMul128(_Ref(), _Bx, _Product._Word[1])}; _Leftover = _Product._Word[0]; } } From 6706d3be2cae39f86cb12542481ed76ee0b86eda Mon Sep 17 00:00:00 2001 From: "Stephan T. Lavavej" Date: Sat, 21 Feb 2026 06:16:52 -0800 Subject: [PATCH 11/17] Drop unused bounds, some with mathematically incorrect comments. --- stl/inc/algorithm | 4 ---- 1 file changed, 4 deletions(-) diff --git a/stl/inc/algorithm b/stl/inc/algorithm index 48bf8c9701a..fce764de752 100644 --- a/stl/inc/algorithm +++ b/stl/inc/algorithm @@ -6198,10 +6198,6 @@ struct _Batched_rng_from_urng { // Threshold bounds for batch sizes based on array size. // These are derived from the paper to minimize expected cost per random value. // Batch size k requires product of k consecutive bounds <= 2^64. - static constexpr uint64_t _Bound_for_batch_6 = 21; // (21)^6 = 85,766,121 < 2^64 / (very conservative) - static constexpr uint64_t _Bound_for_batch_5 = 73; // (73)^5 = 2,073,071,593 < 2^64 - static constexpr uint64_t _Bound_for_batch_4 = 302; // (302)^4 = 8,319,430,096 < 2^64 - static constexpr uint64_t _Bound_for_batch_3 = 2642; // (2642)^3 = 18,454,249,288 < 2^64 static constexpr uint64_t _Bound_for_batch_2 = 4294967296; // 2^32, for batch of 2 _Urng& _Ref; From 16559583ee95b7a72de71a31dd194cf28ea2cbc1 Mon Sep 17 00:00:00 2001 From: "Stephan T. Lavavej" Date: Sat, 21 Feb 2026 06:59:10 -0800 Subject: [PATCH 12/17] Comment nitpicks: RNGs => URNGs, RNG => URNG --- stl/inc/algorithm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stl/inc/algorithm b/stl/inc/algorithm index fce764de752..eccd33abce5 100644 --- a/stl/inc/algorithm +++ b/stl/inc/algorithm @@ -6186,7 +6186,7 @@ namespace ranges { // using only multiplication (no division) in the common case. // Check if a URNG has full 64-bit range [0, 2^64 - 1]. -// Batched random generation is only beneficial for such RNGs. +// Batched random generation is only beneficial for such URNGs. template constexpr bool _Urng_has_full_64bit_range = is_same_v<_Invoke_result_t<_Urng&>, uint64_t> && (_Urng::min) () == 0 && (_Urng::max) () == _Max_limit(); @@ -6603,7 +6603,7 @@ void _Random_shuffle1(_RanIt _First, _RanIt _Last, _RngFn& _RngFunc) { } // Batched shuffle implementation for 64-bit URNGs with full range. -// Uses batched random generation to reduce RNG calls. +// Uses batched random generation to reduce URNG calls. template void _Random_shuffle_batched(_RanIt _First, _RanIt _Last, _Urng& _Func) { // shuffle [_First, _Last) using batched random generation From c5019c50ad58f7e9c71bde43be94f4ca512cadec Mon Sep 17 00:00:00 2001 From: "Stephan T. Lavavej" Date: Sat, 21 Feb 2026 07:28:45 -0800 Subject: [PATCH 13/17] Extract `original`, add `const`. --- .../tests/P0896R4_ranges_alg_shuffle/test.cpp | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/tests/std/tests/P0896R4_ranges_alg_shuffle/test.cpp b/tests/std/tests/P0896R4_ranges_alg_shuffle/test.cpp index 0d8082be24f..30a5da2aec1 100644 --- a/tests/std/tests/P0896R4_ranges_alg_shuffle/test.cpp +++ b/tests/std/tests/P0896R4_ranges_alg_shuffle/test.cpp @@ -78,11 +78,15 @@ void test_urbg() { // COMPILE-ONLY // Test that shuffle produces a valid permutation for various sizes. // This exercises both the batched path (for 64-bit RNGs) and the fallback path. void test_shuffle_permutation() { + const vector original = [] { + vector ret(100); + iota(ret.begin(), ret.end(), 0); + return ret; + }(); + // Test with 64-bit generator (batched random path) { - vector v(100); - iota(v.begin(), v.end(), 0); - vector original = v; + vector v = original; shuffle(v.begin(), v.end(), gen64); @@ -94,9 +98,7 @@ void test_shuffle_permutation() { // Test with ranges::shuffle and 64-bit generator { - vector v(100); - iota(v.begin(), v.end(), 0); - vector original = v; + vector v = original; ranges::shuffle(v, gen64); @@ -108,9 +110,7 @@ void test_shuffle_permutation() { // Test with 32-bit generator (non-batched path) { - vector v(100); - iota(v.begin(), v.end(), 0); - vector original = v; + vector v = original; shuffle(v.begin(), v.end(), gen); @@ -122,9 +122,7 @@ void test_shuffle_permutation() { // Test with ranges::shuffle and 32-bit generator { - vector v(100); - iota(v.begin(), v.end(), 0); - vector original = v; + vector v = original; ranges::shuffle(v, gen); @@ -154,8 +152,8 @@ void test_shuffle_edge_cases() { // Two elements { - vector v = {1, 2}; - vector original = v; + vector v = {1, 2}; + const vector original = v; shuffle(v.begin(), v.end(), gen64); sort(v.begin(), v.end()); assert(v == original); @@ -163,8 +161,8 @@ void test_shuffle_edge_cases() { // Three elements (odd count, tests batching boundary) { - vector v = {1, 2, 3}; - vector original = v; + vector v = {1, 2, 3}; + const vector original = v; shuffle(v.begin(), v.end(), gen64); sort(v.begin(), v.end()); assert(v == original); @@ -172,8 +170,8 @@ void test_shuffle_edge_cases() { // Four elements (even count) { - vector v = {1, 2, 3, 4}; - vector original = v; + vector v = {1, 2, 3, 4}; + const vector original = v; shuffle(v.begin(), v.end(), gen64); sort(v.begin(), v.end()); assert(v == original); @@ -183,7 +181,7 @@ void test_shuffle_edge_cases() { { vector v(10000); iota(v.begin(), v.end(), 0); - vector original = v; + const vector original = v; shuffle(v.begin(), v.end(), gen64); sort(v.begin(), v.end()); assert(v == original); From 318f186b24d4efede122a2923c32c53876945cb8 Mon Sep 17 00:00:00 2001 From: "Stephan T. Lavavej" Date: Sat, 21 Feb 2026 07:31:44 -0800 Subject: [PATCH 14/17] Avoid unnecessary `sorted_v` copy. --- .../tests/P0896R4_ranges_alg_shuffle/test.cpp | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/tests/std/tests/P0896R4_ranges_alg_shuffle/test.cpp b/tests/std/tests/P0896R4_ranges_alg_shuffle/test.cpp index 30a5da2aec1..88f1790de74 100644 --- a/tests/std/tests/P0896R4_ranges_alg_shuffle/test.cpp +++ b/tests/std/tests/P0896R4_ranges_alg_shuffle/test.cpp @@ -91,9 +91,8 @@ void test_shuffle_permutation() { shuffle(v.begin(), v.end(), gen64); // Verify it's still a permutation - vector sorted_v = v; - sort(sorted_v.begin(), sorted_v.end()); - assert(sorted_v == original); + sort(v.begin(), v.end()); + assert(v == original); } // Test with ranges::shuffle and 64-bit generator @@ -103,9 +102,8 @@ void test_shuffle_permutation() { ranges::shuffle(v, gen64); // Verify it's still a permutation - vector sorted_v = v; - sort(sorted_v.begin(), sorted_v.end()); - assert(sorted_v == original); + sort(v.begin(), v.end()); + assert(v == original); } // Test with 32-bit generator (non-batched path) @@ -115,9 +113,8 @@ void test_shuffle_permutation() { shuffle(v.begin(), v.end(), gen); // Verify it's still a permutation - vector sorted_v = v; - sort(sorted_v.begin(), sorted_v.end()); - assert(sorted_v == original); + sort(v.begin(), v.end()); + assert(v == original); } // Test with ranges::shuffle and 32-bit generator @@ -127,9 +124,8 @@ void test_shuffle_permutation() { ranges::shuffle(v, gen); // Verify it's still a permutation - vector sorted_v = v; - sort(sorted_v.begin(), sorted_v.end()); - assert(sorted_v == original); + sort(v.begin(), v.end()); + assert(v == original); } } From 237cf7b08e5eca92583acc7097dd3fcd22f7c844 Mon Sep 17 00:00:00 2001 From: "Stephan T. Lavavej" Date: Sat, 21 Feb 2026 07:32:34 -0800 Subject: [PATCH 15/17] Repeat comments for clarity. --- tests/std/tests/P0896R4_ranges_alg_shuffle/test.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/std/tests/P0896R4_ranges_alg_shuffle/test.cpp b/tests/std/tests/P0896R4_ranges_alg_shuffle/test.cpp index 88f1790de74..3a84e2a1269 100644 --- a/tests/std/tests/P0896R4_ranges_alg_shuffle/test.cpp +++ b/tests/std/tests/P0896R4_ranges_alg_shuffle/test.cpp @@ -95,7 +95,7 @@ void test_shuffle_permutation() { assert(v == original); } - // Test with ranges::shuffle and 64-bit generator + // Test with ranges::shuffle and 64-bit generator (batched random path) { vector v = original; @@ -117,7 +117,7 @@ void test_shuffle_permutation() { assert(v == original); } - // Test with ranges::shuffle and 32-bit generator + // Test with ranges::shuffle and 32-bit generator (non-batched path) { vector v = original; From dfbba0eb5a83b7097949b6fa76083c5d33ed2a3d Mon Sep 17 00:00:00 2001 From: "Stephan T. Lavavej" Date: Sat, 21 Feb 2026 07:40:39 -0800 Subject: [PATCH 16/17] Condense code to make the pattern clearer on a single screen. --- .../tests/P0896R4_ranges_alg_shuffle/test.cpp | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/tests/std/tests/P0896R4_ranges_alg_shuffle/test.cpp b/tests/std/tests/P0896R4_ranges_alg_shuffle/test.cpp index 3a84e2a1269..fde3c07ac48 100644 --- a/tests/std/tests/P0896R4_ranges_alg_shuffle/test.cpp +++ b/tests/std/tests/P0896R4_ranges_alg_shuffle/test.cpp @@ -87,45 +87,33 @@ void test_shuffle_permutation() { // Test with 64-bit generator (batched random path) { vector v = original; - shuffle(v.begin(), v.end(), gen64); - - // Verify it's still a permutation sort(v.begin(), v.end()); - assert(v == original); + assert(v == original); // Verify it's still a permutation } // Test with ranges::shuffle and 64-bit generator (batched random path) { vector v = original; - ranges::shuffle(v, gen64); - - // Verify it's still a permutation sort(v.begin(), v.end()); - assert(v == original); + assert(v == original); // Verify it's still a permutation } // Test with 32-bit generator (non-batched path) { vector v = original; - shuffle(v.begin(), v.end(), gen); - - // Verify it's still a permutation sort(v.begin(), v.end()); - assert(v == original); + assert(v == original); // Verify it's still a permutation } // Test with ranges::shuffle and 32-bit generator (non-batched path) { vector v = original; - ranges::shuffle(v, gen); - - // Verify it's still a permutation sort(v.begin(), v.end()); - assert(v == original); + assert(v == original); // Verify it's still a permutation } } From 82d9e67b9e12c898f28e02a89574ef77361b1ba8 Mon Sep 17 00:00:00 2001 From: "Stephan T. Lavavej" Date: Sat, 21 Feb 2026 07:48:44 -0800 Subject: [PATCH 17/17] Extract `shuffle_is_a_permutation()`, test a large odd size (1729). --- .../tests/P0896R4_ranges_alg_shuffle/test.cpp | 70 +++++-------------- 1 file changed, 19 insertions(+), 51 deletions(-) diff --git a/tests/std/tests/P0896R4_ranges_alg_shuffle/test.cpp b/tests/std/tests/P0896R4_ranges_alg_shuffle/test.cpp index fde3c07ac48..8bb6553d3a2 100644 --- a/tests/std/tests/P0896R4_ranges_alg_shuffle/test.cpp +++ b/tests/std/tests/P0896R4_ranges_alg_shuffle/test.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -117,59 +118,26 @@ void test_shuffle_permutation() { } } +[[nodiscard]] bool shuffle_is_a_permutation(const size_t n) { + vector v(n); + iota(v.begin(), v.end(), 0); + const vector original = v; + shuffle(v.begin(), v.end(), gen64); + sort(v.begin(), v.end()); + return v == original; // have the caller assert() for clearer diagnostics +} + // Test edge cases for shuffle void test_shuffle_edge_cases() { - // Empty range - { - vector v; - shuffle(v.begin(), v.end(), gen64); - assert(v.empty()); - } - - // Single element - { - vector v = {42}; - shuffle(v.begin(), v.end(), gen64); - assert(v.size() == 1); - assert(v[0] == 42); - } - - // Two elements - { - vector v = {1, 2}; - const vector original = v; - shuffle(v.begin(), v.end(), gen64); - sort(v.begin(), v.end()); - assert(v == original); - } - - // Three elements (odd count, tests batching boundary) - { - vector v = {1, 2, 3}; - const vector original = v; - shuffle(v.begin(), v.end(), gen64); - sort(v.begin(), v.end()); - assert(v == original); - } - - // Four elements (even count) - { - vector v = {1, 2, 3, 4}; - const vector original = v; - shuffle(v.begin(), v.end(), gen64); - sort(v.begin(), v.end()); - assert(v == original); - } - - // Large array to ensure batching is effective - { - vector v(10000); - iota(v.begin(), v.end(), 0); - const vector original = v; - shuffle(v.begin(), v.end(), gen64); - sort(v.begin(), v.end()); - assert(v == original); - } + // Test both even and odd sizes to exercise the batching boundary. + // Test large sizes to ensure batching is effective. + assert(shuffle_is_a_permutation(0)); + assert(shuffle_is_a_permutation(1)); + assert(shuffle_is_a_permutation(2)); + assert(shuffle_is_a_permutation(3)); + assert(shuffle_is_a_permutation(4)); + assert(shuffle_is_a_permutation(1729)); + assert(shuffle_is_a_permutation(10000)); } int main() {