From 7936a9f498eebee41b463e4a29b5b05c13899998 Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Thu, 26 Dec 2024 01:18:59 +0100 Subject: [PATCH 01/24] add `shuffle(::NTuple)` to `Random` Fixes #56728 --- stdlib/Random/src/misc.jl | 30 ++++++++++++++++++++++++++++++ stdlib/Random/test/runtests.jl | 28 ++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/stdlib/Random/src/misc.jl b/stdlib/Random/src/misc.jl index 0f113e8153f04..315257bf9d555 100644 --- a/stdlib/Random/src/misc.jl +++ b/stdlib/Random/src/misc.jl @@ -183,6 +183,36 @@ ltm52(n::Int, mask::Int=nextpow(2, n)-1) = LessThan(n-1, Masked(mask, UInt52Raw( ## shuffle & shuffle! +function _tuple_without_element_at_index(tup::(Tuple{T, Vararg{T, N}} where {T}), i::Int) where {N} + clo = let tup = tup, i = i + function closure(j::Int) + k = if j < i + j + else + j + 1 + end + tup[k] + end + end + ntuple(clo, Val{N}()) +end + +function shuffle(rng::AbstractRNG, tup::(Tuple{Vararg{T, N}} where {T})) where {N} + if tup isa Tuple{Any, Vararg} + let i = rand(rng, Base.OneTo(N)) # Fisher–Yates shuffle + s = _tuple_without_element_at_index(tup, i) + t = shuffle(rng, s) + (tup[i], t...) + end + else + tup + end::typeof(tup) +end + +function shuffle(tup::NTuple) + shuffle(default_rng(), tup) +end + """ shuffle!([rng=default_rng(),] v::AbstractArray) diff --git a/stdlib/Random/test/runtests.jl b/stdlib/Random/test/runtests.jl index e929cd2d8a517..aec1dacd17546 100644 --- a/stdlib/Random/test/runtests.jl +++ b/stdlib/Random/test/runtests.jl @@ -1071,6 +1071,34 @@ end @test maximum(m) <= 0.106 end +@testset "`shuffle(::NTuple)`" begin + @testset "sorted" begin + for n ∈ 0:20 + tup = ntuple(identity, n) + @test tup === sort(@inferred shuffle(tup)) + end + end + @testset "not identity" begin + function is_not_identity_at_least_once() + function f(::Any) + tup = ntuple(identity, 9) + tup !== shuffle(tup) + end + @test any(f, 1:1000000) + end + is_not_identity_at_least_once() + end + @testset "no heap allocation" begin + function no_heap_allocation(tup) + @test iszero(@allocated shuffle(tup)) + end + for n ∈ 0:9 + tup = ntuple(identity, n) + no_heap_allocation(tup) + end + end +end + # issue #42752 # test that running finalizers that launch tasks doesn't change RNG stream function f42752(do_gc::Bool, cell = (()->Any[[]])()) From f3fb9f9a39020ddc541f9e6882fc37fa929819b7 Mon Sep 17 00:00:00 2001 From: Neven Sajko <4944410+nsajko@users.noreply.github.com> Date: Fri, 9 May 2025 12:30:54 +0200 Subject: [PATCH 02/24] rely on `shuffle!` and on escape analysis instead of on recursion --- stdlib/Random/src/misc.jl | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/stdlib/Random/src/misc.jl b/stdlib/Random/src/misc.jl index 315257bf9d555..d0fe42be99a56 100644 --- a/stdlib/Random/src/misc.jl +++ b/stdlib/Random/src/misc.jl @@ -183,26 +183,19 @@ ltm52(n::Int, mask::Int=nextpow(2, n)-1) = LessThan(n-1, Masked(mask, UInt52Raw( ## shuffle & shuffle! -function _tuple_without_element_at_index(tup::(Tuple{T, Vararg{T, N}} where {T}), i::Int) where {N} - clo = let tup = tup, i = i - function closure(j::Int) - k = if j < i - j - else - j + 1 - end - tup[k] - end - end - ntuple(clo, Val{N}()) -end - function shuffle(rng::AbstractRNG, tup::(Tuple{Vararg{T, N}} where {T})) where {N} if tup isa Tuple{Any, Vararg} - let i = rand(rng, Base.OneTo(N)) # Fisher–Yates shuffle - s = _tuple_without_element_at_index(tup, i) - t = shuffle(rng, s) - (tup[i], t...) + @inline let # `@inline` and `@inbounds` are here to help escape analysis + clo = @inbounds let mem = Memory{UInt16}(undef, N) # use `UInt16` to save stack space/prevent heap allocation + copyto!(mem, Base.OneTo(N)) + shuffle!(mem) + let mem = mem, tup = tup + function closure(i::Int) + @inbounds tup[mem[i]] + end + end + end + ntuple(clo, Val{N}()) end else tup From dc162c38b46f5730527fc761402ab00107349b41 Mon Sep 17 00:00:00 2001 From: Neven Sajko <4944410+nsajko@users.noreply.github.com> Date: Fri, 9 May 2025 12:33:31 +0200 Subject: [PATCH 03/24] bugfix: use provided pseudorandom generator --- stdlib/Random/src/misc.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stdlib/Random/src/misc.jl b/stdlib/Random/src/misc.jl index d0fe42be99a56..2679088704767 100644 --- a/stdlib/Random/src/misc.jl +++ b/stdlib/Random/src/misc.jl @@ -188,7 +188,7 @@ function shuffle(rng::AbstractRNG, tup::(Tuple{Vararg{T, N}} where {T})) where { @inline let # `@inline` and `@inbounds` are here to help escape analysis clo = @inbounds let mem = Memory{UInt16}(undef, N) # use `UInt16` to save stack space/prevent heap allocation copyto!(mem, Base.OneTo(N)) - shuffle!(mem) + shuffle!(rng, mem) let mem = mem, tup = tup function closure(i::Int) @inbounds tup[mem[i]] From 2eee6e61c9ed2cc1d5d492e505583169387bb576 Mon Sep 17 00:00:00 2001 From: Neven Sajko <4944410+nsajko@users.noreply.github.com> Date: Fri, 9 May 2025 12:37:08 +0200 Subject: [PATCH 04/24] simplify using `randperm!` as per rfourqet --- stdlib/Random/src/misc.jl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/stdlib/Random/src/misc.jl b/stdlib/Random/src/misc.jl index 2679088704767..534fbb3df943c 100644 --- a/stdlib/Random/src/misc.jl +++ b/stdlib/Random/src/misc.jl @@ -187,8 +187,7 @@ function shuffle(rng::AbstractRNG, tup::(Tuple{Vararg{T, N}} where {T})) where { if tup isa Tuple{Any, Vararg} @inline let # `@inline` and `@inbounds` are here to help escape analysis clo = @inbounds let mem = Memory{UInt16}(undef, N) # use `UInt16` to save stack space/prevent heap allocation - copyto!(mem, Base.OneTo(N)) - shuffle!(rng, mem) + randperm!(rng, mem) let mem = mem, tup = tup function closure(i::Int) @inbounds tup[mem[i]] From 8a8efffa6884736cadd9fd2c747a4656ef29aa2b Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Fri, 9 May 2025 12:44:27 +0200 Subject: [PATCH 05/24] delete unnecessary branch --- stdlib/Random/src/misc.jl | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/stdlib/Random/src/misc.jl b/stdlib/Random/src/misc.jl index 534fbb3df943c..419d5791739a0 100644 --- a/stdlib/Random/src/misc.jl +++ b/stdlib/Random/src/misc.jl @@ -184,7 +184,6 @@ ltm52(n::Int, mask::Int=nextpow(2, n)-1) = LessThan(n-1, Masked(mask, UInt52Raw( ## shuffle & shuffle! function shuffle(rng::AbstractRNG, tup::(Tuple{Vararg{T, N}} where {T})) where {N} - if tup isa Tuple{Any, Vararg} @inline let # `@inline` and `@inbounds` are here to help escape analysis clo = @inbounds let mem = Memory{UInt16}(undef, N) # use `UInt16` to save stack space/prevent heap allocation randperm!(rng, mem) @@ -194,11 +193,8 @@ function shuffle(rng::AbstractRNG, tup::(Tuple{Vararg{T, N}} where {T})) where { end end end - ntuple(clo, Val{N}()) + ntuple(clo, Val{N}())::typeof(tup) end - else - tup - end::typeof(tup) end function shuffle(tup::NTuple) From fcec46688081e917137273d31fa3699cb987bb5f Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Fri, 9 May 2025 12:47:08 +0200 Subject: [PATCH 06/24] reindent --- stdlib/Random/src/misc.jl | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/stdlib/Random/src/misc.jl b/stdlib/Random/src/misc.jl index 419d5791739a0..41a761ee742de 100644 --- a/stdlib/Random/src/misc.jl +++ b/stdlib/Random/src/misc.jl @@ -184,17 +184,17 @@ ltm52(n::Int, mask::Int=nextpow(2, n)-1) = LessThan(n-1, Masked(mask, UInt52Raw( ## shuffle & shuffle! function shuffle(rng::AbstractRNG, tup::(Tuple{Vararg{T, N}} where {T})) where {N} - @inline let # `@inline` and `@inbounds` are here to help escape analysis - clo = @inbounds let mem = Memory{UInt16}(undef, N) # use `UInt16` to save stack space/prevent heap allocation - randperm!(rng, mem) - let mem = mem, tup = tup - function closure(i::Int) - @inbounds tup[mem[i]] - end + @inline let # `@inline` and `@inbounds` are here to help escape analysis + clo = @inbounds let mem = Memory{UInt16}(undef, N) # use `UInt16` to save stack space/prevent heap allocation + randperm!(rng, mem) + let mem = mem, tup = tup + function closure(i::Int) + @inbounds tup[mem[i]] end end - ntuple(clo, Val{N}())::typeof(tup) end + ntuple(clo, Val{N}())::typeof(tup) + end end function shuffle(tup::NTuple) From 9e9c1a538621cad2670b614b8b48f3808de5e39d Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Fri, 9 May 2025 12:51:14 +0200 Subject: [PATCH 07/24] add news --- NEWS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NEWS.md b/NEWS.md index 1e4e24b67a4db..dcf2b13c12214 100644 --- a/NEWS.md +++ b/NEWS.md @@ -65,6 +65,8 @@ Standard library changes * `randperm!` and `randcycle!` now support non-`Array` `AbstractArray` inputs, assuming they are mutable and their indices are one-based ([#58596]). +* `shuffle` now may take an argument of `NTuple` value ([#56906]). + #### REPL * The display of `AbstractChar`s in the main REPL mode now includes LaTeX input information like what is shown in help mode ([#58181]). From ed3e09a86566f12fb4345f09af6d5e67c35c3d6c Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Fri, 9 May 2025 12:54:43 +0200 Subject: [PATCH 08/24] update doc string, and apply it to the function instead of to a method --- stdlib/Random/src/misc.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/stdlib/Random/src/misc.jl b/stdlib/Random/src/misc.jl index 41a761ee742de..d59734e3afd8f 100644 --- a/stdlib/Random/src/misc.jl +++ b/stdlib/Random/src/misc.jl @@ -256,7 +256,7 @@ end shuffle!(a::AbstractArray) = shuffle!(default_rng(), a) """ - shuffle([rng=default_rng(),] v::AbstractArray) + shuffle([rng=default_rng(),] v::Union{Tuple,AbstractArray}) Return a randomly permuted copy of `v`. The optional `rng` argument specifies a random number generator (see [Random Numbers](@ref)). @@ -279,6 +279,8 @@ julia> shuffle(Xoshiro(123), Vector(1:10)) 7 ``` """ +function shuffle end + shuffle(r::AbstractRNG, a::AbstractArray) = shuffle!(r, copymutable(a)) shuffle(a::AbstractArray) = shuffle(default_rng(), a) From 5b50ae3f5f760aa7ed1a85b7c0ad0e4070fe7938 Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Fri, 9 May 2025 13:03:47 +0200 Subject: [PATCH 09/24] use narrower type when possible as per rfourqet --- stdlib/Random/src/misc.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/stdlib/Random/src/misc.jl b/stdlib/Random/src/misc.jl index d59734e3afd8f..6719d0f019b36 100644 --- a/stdlib/Random/src/misc.jl +++ b/stdlib/Random/src/misc.jl @@ -185,7 +185,8 @@ ltm52(n::Int, mask::Int=nextpow(2, n)-1) = LessThan(n-1, Masked(mask, UInt52Raw( function shuffle(rng::AbstractRNG, tup::(Tuple{Vararg{T, N}} where {T})) where {N} @inline let # `@inline` and `@inbounds` are here to help escape analysis - clo = @inbounds let mem = Memory{UInt16}(undef, N) # use `UInt16` to save stack space/prevent heap allocation + Ind = (N <= 0xff) ? UInt8 : UInt16 # use a narrow integer type to save stack space and prevent heap allocation + clo = @inbounds let mem = Memory{Ind}(undef, N) randperm!(rng, mem) let mem = mem, tup = tup function closure(i::Int) From 90f1d2dfe4691d3be4797d6c17b84294f258e8da Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Fri, 9 May 2025 13:08:10 +0200 Subject: [PATCH 10/24] de-hardcode `typemax(UInt8)` --- stdlib/Random/src/misc.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stdlib/Random/src/misc.jl b/stdlib/Random/src/misc.jl index 6719d0f019b36..8bfe978c0278f 100644 --- a/stdlib/Random/src/misc.jl +++ b/stdlib/Random/src/misc.jl @@ -185,7 +185,7 @@ ltm52(n::Int, mask::Int=nextpow(2, n)-1) = LessThan(n-1, Masked(mask, UInt52Raw( function shuffle(rng::AbstractRNG, tup::(Tuple{Vararg{T, N}} where {T})) where {N} @inline let # `@inline` and `@inbounds` are here to help escape analysis - Ind = (N <= 0xff) ? UInt8 : UInt16 # use a narrow integer type to save stack space and prevent heap allocation + Ind = (N <= typemax(UInt8)) ? UInt8 : UInt16 # use a narrow integer type to save stack space and prevent heap allocation clo = @inbounds let mem = Memory{Ind}(undef, N) randperm!(rng, mem) let mem = mem, tup = tup From 6e80b0d6d3a697a37570bc85d3c4578c0005a878 Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Fri, 9 May 2025 13:10:26 +0200 Subject: [PATCH 11/24] safer --- stdlib/Random/src/misc.jl | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/stdlib/Random/src/misc.jl b/stdlib/Random/src/misc.jl index 8bfe978c0278f..ee924057ee1f6 100644 --- a/stdlib/Random/src/misc.jl +++ b/stdlib/Random/src/misc.jl @@ -185,7 +185,14 @@ ltm52(n::Int, mask::Int=nextpow(2, n)-1) = LessThan(n-1, Masked(mask, UInt52Raw( function shuffle(rng::AbstractRNG, tup::(Tuple{Vararg{T, N}} where {T})) where {N} @inline let # `@inline` and `@inbounds` are here to help escape analysis - Ind = (N <= typemax(UInt8)) ? UInt8 : UInt16 # use a narrow integer type to save stack space and prevent heap allocation + # use a narrow integer type to save stack space and prevent heap allocation + Ind = if N ≤ typemax(UInt8) + UInt8 + elseif N ≤ typemax(UInt16) + UInt16 + else + UInt + end clo = @inbounds let mem = Memory{Ind}(undef, N) randperm!(rng, mem) let mem = mem, tup = tup From 18ededb7b3ba77a9e3f540dbe8bba73123ff7acc Mon Sep 17 00:00:00 2001 From: Neven Sajko <4944410+nsajko@users.noreply.github.com> Date: Fri, 9 May 2025 13:51:44 +0200 Subject: [PATCH 12/24] use `shuffle!` instead of `randperm!` again --- stdlib/Random/src/misc.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stdlib/Random/src/misc.jl b/stdlib/Random/src/misc.jl index ee924057ee1f6..c12bf5740fa8f 100644 --- a/stdlib/Random/src/misc.jl +++ b/stdlib/Random/src/misc.jl @@ -194,7 +194,7 @@ function shuffle(rng::AbstractRNG, tup::(Tuple{Vararg{T, N}} where {T})) where { UInt end clo = @inbounds let mem = Memory{Ind}(undef, N) - randperm!(rng, mem) + shuffle!(rng, copyto!(mem, Base.OneTo(N))) # just use `randperm!` once it supports `Memory` let mem = mem, tup = tup function closure(i::Int) @inbounds tup[mem[i]] From c6c39973303caf5d7b191a0165e45e673417cece Mon Sep 17 00:00:00 2001 From: Neven Sajko <4944410+nsajko@users.noreply.github.com> Date: Fri, 9 May 2025 14:25:44 +0200 Subject: [PATCH 13/24] relax one-arg method Co-authored-by: Rafael Fourquet --- stdlib/Random/src/misc.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stdlib/Random/src/misc.jl b/stdlib/Random/src/misc.jl index c12bf5740fa8f..53f70beff7c24 100644 --- a/stdlib/Random/src/misc.jl +++ b/stdlib/Random/src/misc.jl @@ -290,7 +290,7 @@ julia> shuffle(Xoshiro(123), Vector(1:10)) function shuffle end shuffle(r::AbstractRNG, a::AbstractArray) = shuffle!(r, copymutable(a)) -shuffle(a::AbstractArray) = shuffle(default_rng(), a) +shuffle(a::Union{Tuple, AbstractArray}) = shuffle(default_rng(), a) shuffle(r::AbstractRNG, a::Base.OneTo) = randperm(r, last(a)) From 4ab6f864252682e0aba8ece1c9aa6964a3279396 Mon Sep 17 00:00:00 2001 From: Neven Sajko <4944410+nsajko@users.noreply.github.com> Date: Fri, 9 May 2025 14:27:12 +0200 Subject: [PATCH 14/24] simpler dispatch constraint Co-authored-by: Rafael Fourquet --- stdlib/Random/src/misc.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stdlib/Random/src/misc.jl b/stdlib/Random/src/misc.jl index 53f70beff7c24..d20e17fc128cd 100644 --- a/stdlib/Random/src/misc.jl +++ b/stdlib/Random/src/misc.jl @@ -183,7 +183,7 @@ ltm52(n::Int, mask::Int=nextpow(2, n)-1) = LessThan(n-1, Masked(mask, UInt52Raw( ## shuffle & shuffle! -function shuffle(rng::AbstractRNG, tup::(Tuple{Vararg{T, N}} where {T})) where {N} +function shuffle(rng::AbstractRNG, tup::NTuple{N}) where {N} @inline let # `@inline` and `@inbounds` are here to help escape analysis # use a narrow integer type to save stack space and prevent heap allocation Ind = if N ≤ typemax(UInt8) From d7191ef5e0674912a59e99d83cdc8ad1063134cf Mon Sep 17 00:00:00 2001 From: Neven Sajko <4944410+nsajko@users.noreply.github.com> Date: Fri, 9 May 2025 14:31:00 +0200 Subject: [PATCH 15/24] delete now unnecessary method Co-authored-by: Rafael Fourquet --- stdlib/Random/src/misc.jl | 3 --- 1 file changed, 3 deletions(-) diff --git a/stdlib/Random/src/misc.jl b/stdlib/Random/src/misc.jl index d20e17fc128cd..abdb6eb479a75 100644 --- a/stdlib/Random/src/misc.jl +++ b/stdlib/Random/src/misc.jl @@ -205,9 +205,6 @@ function shuffle(rng::AbstractRNG, tup::NTuple{N}) where {N} end end -function shuffle(tup::NTuple) - shuffle(default_rng(), tup) -end """ shuffle!([rng=default_rng(),] v::AbstractArray) From 8f3800f82b061dc6f1aff0663d4e7c35614f6713 Mon Sep 17 00:00:00 2001 From: Neven Sajko <4944410+nsajko@users.noreply.github.com> Date: Fri, 9 May 2025 14:32:08 +0200 Subject: [PATCH 16/24] delete extraneous newline --- stdlib/Random/src/misc.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/stdlib/Random/src/misc.jl b/stdlib/Random/src/misc.jl index abdb6eb479a75..c2a868eadf02f 100644 --- a/stdlib/Random/src/misc.jl +++ b/stdlib/Random/src/misc.jl @@ -205,7 +205,6 @@ function shuffle(rng::AbstractRNG, tup::NTuple{N}) where {N} end end - """ shuffle!([rng=default_rng(),] v::AbstractArray) From ba162dc2ac0db0c7c46e1f79169570b3f35f8003 Mon Sep 17 00:00:00 2001 From: Neven Sajko <4944410+nsajko@users.noreply.github.com> Date: Fri, 9 May 2025 15:55:26 +0200 Subject: [PATCH 17/24] omit no alloc test set --- stdlib/Random/test/runtests.jl | 9 --------- 1 file changed, 9 deletions(-) diff --git a/stdlib/Random/test/runtests.jl b/stdlib/Random/test/runtests.jl index aec1dacd17546..3d745dde4bd53 100644 --- a/stdlib/Random/test/runtests.jl +++ b/stdlib/Random/test/runtests.jl @@ -1088,15 +1088,6 @@ end end is_not_identity_at_least_once() end - @testset "no heap allocation" begin - function no_heap_allocation(tup) - @test iszero(@allocated shuffle(tup)) - end - for n ∈ 0:9 - tup = ntuple(identity, n) - no_heap_allocation(tup) - end - end end # issue #42752 From 912ec6c7c446f070c0c81297f17ea18ca8e35787 Mon Sep 17 00:00:00 2001 From: Neven Sajko <4944410+nsajko@users.noreply.github.com> Date: Mon, 12 May 2025 11:59:47 +0200 Subject: [PATCH 18/24] fix dispatch constraint: restrict to `NTuple` Co-authored-by: Rafael Fourquet --- stdlib/Random/src/misc.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stdlib/Random/src/misc.jl b/stdlib/Random/src/misc.jl index c2a868eadf02f..8707d10215484 100644 --- a/stdlib/Random/src/misc.jl +++ b/stdlib/Random/src/misc.jl @@ -260,7 +260,7 @@ end shuffle!(a::AbstractArray) = shuffle!(default_rng(), a) """ - shuffle([rng=default_rng(),] v::Union{Tuple,AbstractArray}) + shuffle([rng=default_rng(),] v::Union{NTuple,AbstractArray}) Return a randomly permuted copy of `v`. The optional `rng` argument specifies a random number generator (see [Random Numbers](@ref)). From 35feb2dda7eac23394aeb5609433f623c4c6030d Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Mon, 12 May 2025 12:08:54 +0200 Subject: [PATCH 19/24] add compat annotation to doc string --- stdlib/Random/src/misc.jl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/stdlib/Random/src/misc.jl b/stdlib/Random/src/misc.jl index 8707d10215484..22992d18e5790 100644 --- a/stdlib/Random/src/misc.jl +++ b/stdlib/Random/src/misc.jl @@ -267,6 +267,9 @@ number generator (see [Random Numbers](@ref)). To permute `v` in-place, see [`shuffle!`](@ref). To obtain randomly permuted indices, see [`randperm`](@ref). +!!! compat "Julia 1.13" + Shuffling an `NTuple` value requires Julia v1.13 or above. + # Examples ```jldoctest julia> shuffle(Xoshiro(123), Vector(1:10)) From bed23af81c9b4af26a8e96a3c359d63b780f5cc4 Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Sat, 31 May 2025 12:30:51 +0200 Subject: [PATCH 20/24] adjust "not identity at least once" test --- stdlib/Random/test/runtests.jl | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/stdlib/Random/test/runtests.jl b/stdlib/Random/test/runtests.jl index 3d745dde4bd53..6403fbfe958c3 100644 --- a/stdlib/Random/test/runtests.jl +++ b/stdlib/Random/test/runtests.jl @@ -1079,14 +1079,12 @@ end end end @testset "not identity" begin - function is_not_identity_at_least_once() - function f(::Any) - tup = ntuple(identity, 9) - tup !== shuffle(tup) - end - @test any(f, 1:1000000) + function shuffle_is_identity() + tup = ntuple(identity, 9) + tup === shuffle(tup) end - is_not_identity_at_least_once() + # shuffling may behave as the identity sometimes, but if it doesn't manage to actually reorder some of the elements at least once, something is wrong + @test any((_ -> !shuffle_is_identity()), 1:1000000) end end From 9944d1c7fcea96faaa8073e5bad2ddb816745743 Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Sat, 31 May 2025 13:04:27 +0200 Subject: [PATCH 21/24] expand comment --- stdlib/Random/src/misc.jl | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/stdlib/Random/src/misc.jl b/stdlib/Random/src/misc.jl index 22992d18e5790..a95ba528d95f9 100644 --- a/stdlib/Random/src/misc.jl +++ b/stdlib/Random/src/misc.jl @@ -184,7 +184,17 @@ ltm52(n::Int, mask::Int=nextpow(2, n)-1) = LessThan(n-1, Masked(mask, UInt52Raw( ## shuffle & shuffle! function shuffle(rng::AbstractRNG, tup::NTuple{N}) where {N} - @inline let # `@inline` and `@inbounds` are here to help escape analysis + # `@inline` and `@inbounds` are here to help escape analysis eliminate the `Memory` allocation + # + # * `@inline` might be necessary because escape analysis relies on everything + # touching the `Memory` being inlined because there's no interprocedural escape + # analysis yet, relevant WIP PR: https://github.com/JuliaLang/julia/pull/56849 + # + # * `@inbounds` might be necessary because escape analysis requires any throws of + # `BoundsError` to be eliminated as dead code, because `BoundsError` stores the + # array itself, making the throw escape the array from the function, relevant + # WIP PR: https://github.com/JuliaLang/julia/pull/56167 + @inline let # use a narrow integer type to save stack space and prevent heap allocation Ind = if N ≤ typemax(UInt8) UInt8 From 6ebd824414da74e0eae574611a9f38e0feed373c Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Sat, 31 May 2025 13:04:56 +0200 Subject: [PATCH 22/24] simplify syntax, less `let`, etc --- stdlib/Random/src/misc.jl | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/stdlib/Random/src/misc.jl b/stdlib/Random/src/misc.jl index a95ba528d95f9..a40c8d9e0fff5 100644 --- a/stdlib/Random/src/misc.jl +++ b/stdlib/Random/src/misc.jl @@ -203,15 +203,12 @@ function shuffle(rng::AbstractRNG, tup::NTuple{N}) where {N} else UInt end - clo = @inbounds let mem = Memory{Ind}(undef, N) - shuffle!(rng, copyto!(mem, Base.OneTo(N))) # just use `randperm!` once it supports `Memory` - let mem = mem, tup = tup - function closure(i::Int) - @inbounds tup[mem[i]] - end - end + # TODO: use `randperm!` instead of `copyto!` from a unit range once `randperm!` supports `Memory` + mem = @inbounds shuffle!(rng, copyto!(Memory{Ind}(undef, N), Base.OneTo(N))) + function closure(i::Int) + @inbounds tup[mem[i]] end - ntuple(clo, Val{N}())::typeof(tup) + ntuple(closure, Val{N}())::typeof(tup) end end From d1ea278dbadb4d8dfe9d899cd36ed31eaf07be4b Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Tue, 10 Jun 2025 18:05:43 +0200 Subject: [PATCH 23/24] use `randperm!` again, as it supports `Memory` now The PR #58596 was merged. --- stdlib/Random/src/misc.jl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/stdlib/Random/src/misc.jl b/stdlib/Random/src/misc.jl index a40c8d9e0fff5..f437d17bf1103 100644 --- a/stdlib/Random/src/misc.jl +++ b/stdlib/Random/src/misc.jl @@ -203,8 +203,7 @@ function shuffle(rng::AbstractRNG, tup::NTuple{N}) where {N} else UInt end - # TODO: use `randperm!` instead of `copyto!` from a unit range once `randperm!` supports `Memory` - mem = @inbounds shuffle!(rng, copyto!(Memory{Ind}(undef, N), Base.OneTo(N))) + mem = @inbounds randperm!(rng, Memory{Ind}(undef, N)) function closure(i::Int) @inbounds tup[mem[i]] end From f9dd73dfb99ee9f4ecde41f13cd613811190c2db Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Tue, 10 Jun 2025 18:14:25 +0200 Subject: [PATCH 24/24] restrict the one-arg method from `Tuple` to `NTuple` --- stdlib/Random/src/misc.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stdlib/Random/src/misc.jl b/stdlib/Random/src/misc.jl index f437d17bf1103..395137b50abfd 100644 --- a/stdlib/Random/src/misc.jl +++ b/stdlib/Random/src/misc.jl @@ -295,7 +295,7 @@ julia> shuffle(Xoshiro(123), Vector(1:10)) function shuffle end shuffle(r::AbstractRNG, a::AbstractArray) = shuffle!(r, copymutable(a)) -shuffle(a::Union{Tuple, AbstractArray}) = shuffle(default_rng(), a) +shuffle(a::Union{NTuple, AbstractArray}) = shuffle(default_rng(), a) shuffle(r::AbstractRNG, a::Base.OneTo) = randperm(r, last(a))