Skip to content

Commit 5facdd1

Browse files
committed
implement "nearly division less" algorithm for rand(a:b)
Cf. https://arxiv.org/abs/1805.10941. Closes #20582, #29004.
1 parent e103478 commit 5facdd1

File tree

2 files changed

+55
-5
lines changed

2 files changed

+55
-5
lines changed

stdlib/Random/src/generation.jl

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -166,13 +166,15 @@ end
166166

167167
### BitInteger
168168

169-
# there are two implemented samplers for unit ranges, which assume that Float64 (i.e.
170-
# 52 random bits) is the native type for the RNG:
171-
# 1) "Fast", which is the most efficient when the underlying RNG produces rand(Float64)
172-
# "fast enough". The tradeoff is faster creation of the sampler, but more
173-
# consumption of entropy bits
169+
# there are three implemented samplers for unit ranges, the two first of which
170+
# assume that Float64 (i.e. 52 random bits) is the native type for the RNG:
171+
# 1) "Fast", which is most efficient when the underlying RNG produces rand(Float64)
172+
# "fast enough". The tradeoff is faster creation of the sampler, but more
173+
# consumption of entropy bits
174174
# 2) "Default" which tries to use as few entropy bits as possible, at the cost of a
175175
# a bigger upfront price associated with the creation of the sampler
176+
# 3) "Nearly Division Less", which is the fastest algorithm for types of size
177+
# up to 64 bits. This will be the default in a future realease.
176178

177179
#### helper functions
178180

@@ -295,6 +297,45 @@ function rand(rng::AbstractRNG, sp::SamplerRangeInt{T,UInt128}) where T<:BitInte
295297
return ((sp.a % UInt128) + rem_knuth(x, sp.k)) % T
296298
end
297299

300+
#### Nearly Division Less
301+
302+
# cf. https://arxiv.org/abs/1805.10941
303+
304+
struct SamplerRangeNDL{U<:Unsigned,T} <: Sampler{T}
305+
a::T # first element of the range
306+
s::U # range length or zero for full range
307+
end
308+
309+
function SamplerRangeNDL(r::AbstractUnitRange{T}) where {T}
310+
isempty(r) && throw(ArgumentError("range must be non-empty"))
311+
a = first(r)
312+
U = uint_sup(T)
313+
s = (last(r) - first(r)) % unsigned(T) % U + one(U) # overflow ok
314+
# mod(-s, s) could be put in the Sampler object for repeated calls, but
315+
# this would be an advantage only for very big s and number of calls
316+
SamplerRangeNDL(a, s)
317+
end
318+
319+
function rand(rng::AbstractRNG, sp::SamplerRangeNDL{U,T}) where {U,T}
320+
s = sp.s
321+
x = widen(rand(rng, U))
322+
m = x * s
323+
l = m % U
324+
if l < s
325+
t = mod(-s, s)
326+
while l < t
327+
x = widen(rand(rng, U))
328+
m = x * s
329+
l = m % U
330+
end
331+
end
332+
(s == 0 ? x : m >> (8*sizeof(U))) % T + sp.a
333+
end
334+
335+
# API until this is made the default
336+
fast(r::AbstractUnitRange{<:Base.BitInteger64}) = SamplerRangeNDL(r)
337+
fast(r::AbstractArray) = Random.SamplerSimple(r, fast(firstindex(r):lastindex(r)))
338+
298339

299340
### BigInt
300341

stdlib/Random/test/runtests.jl

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -810,3 +810,12 @@ end
810810
@testset "RNGs broadcast as scalars: T" for T in (MersenneTwister, RandomDevice)
811811
@test length.(rand.(T(), 1:3)) == 1:3
812812
end
813+
814+
@testset "fast(a:b)" begin
815+
for bounds = (rand(Int, 2), rand(-1000:1000, 2))
816+
a, b = minmax(bounds...)
817+
a == b && continue
818+
@test rand(Random.fast(a:b)) a:b
819+
@test rand(Random.fast(a:1:b)) a:b
820+
end
821+
end

0 commit comments

Comments
 (0)