 # Fast random number generator for RGBs

I would like to generate very fast pseudo random images. The quality of the random numbers to generate random RGBs are not very important to be completely independent. But they sould look “random” for a human eye.

Is there a faster way to generate random RGBs as “rand(RGB{Float32},1080,1980)” ?

Well, this is already > 6x faster:

``````function f2()
M = Matrix{RGB{Float32}}(undef, 1080, 1980)
@inbounds for j in 1:1980
for i in 1:1080
M[i,j] = RGB(rand(), rand(), rand())
end
end
return M
end
``````
``````julia> @btime f1(); # f1() = rand(RGB{Float32},1080,1980)
137.335 ms (3 allocations: 24.47 MiB)

julia> @btime f2();
20.709 ms (2 allocations: 24.47 MiB)
``````

But of course, you should probably think about algorithmic improvements, i.e. how to utilise the fact that statistical randomness is not the primary objective. On the purely technical side, you might try other RNGs, for example from https://github.com/sunoru/RandomNumbers.jl.

Update:

``````using RandomNumbers
rng_xor = RandomNumbers.Xorshifts.Xoroshiro128Star()

function f2_rng(rng)
M = Matrix{RGB{Float32}}(undef, 1080, 1980)
@inbounds for j in 1:1980
for i in 1:1080
M[i,j] = RGB(rand(rng), rand(rng), rand(rng))
end
end
return M
end
``````

I find

``````julia> @btime f2_rng(\$rng_xor);
8.903 ms (2 allocations: 24.47 MiB)``````
5 Likes

This is a bit simpler than `f2`, while retaining the same performance:

``````julia> @btime f2();
27.606 ms (2 allocations: 24.47 MiB)

julia> function f3()
M = Matrix{RGB{Float32}}(undef, 1080, 1980)
@. M = RGB(rand(), rand(), rand())
return M
end
f3 (generic function with 1 method)

julia> @btime f3();
28.184 ms (2 allocations: 24.47 MiB)
``````

ButiIf you want performance you need to explicitly pass the RNG:

``````julia> rng = Random.default_rng()
MersenneTwister(0x1367e9fe8e33ee99f87edfd09c2e5904, (0, 7916357112, 7916356110, 690))

julia> function f4(rng)
M = Matrix{RGB{Float32}}(undef, 1080, 1980)
@. M = RGB(rand(rng), rand(rng), rand(rng))
return M
end
f4 (generic function with 1 method)

julia> @btime f4(\$rng);
11.616 ms (2 allocations: 24.47 MiB)

julia> using RandomNumbers

julia> rng_xor = RandomNumbers.Xorshifts.Xoroshiro128Star()
RandomNumbers.Xorshifts.Xoroshiro128Star(0x5f9b1be270242ed9, 0x4ddeffa1af6dbc23)

julia> @btime f4(\$rng_xor);
11.991 ms (2 allocations: 24.47 MiB)
``````
5 Likes

Oh, interesting. I didn’t know that passing the rng explicitly can have such a big impact.

Yes, the speedup you’re seeing with Xor is only due to the fact you passed the RNG explicitly. Accessing the default global RNG takes time.

8 Likes

A good explanation of why one needs to pass rng explicitly in In Julia 1.5 is here: https://bkamins.github.io/julialang/2020/11/20/rand.html

6 Likes

Thank you all! I learned a lot here!

Hi,

I would like to follow. Does anyone knows about an ultra-fast approach to generate random values in mod-3 algebra, i.e. 0,1,2? So far, I use

``````rand(rng, 0:2),
``````

where `rng = Xorshift64Star`. I wonder if there might be something even faster?

Thanks a lot.

that should be full speed. (although technically it might be slightly faster to generate a small `SVector` of them since it should then be able to use fewer bits of entropy)

Thanks a lot.

Too bad there is no silver bullet.

There is a specialization for small tuples, such that `rand(rng, (1, 2))` is faster than `rand(rng, 1:2)`. For three elements the benefit seem negligible though.

`rand(RGB{Float32},1080,1980)` is now faster than everything else recommended in this thread.

It’s been fixed in the (not yet released) commit Use sampler-based Random API (#222) · JuliaGraphics/ColorTypes.jl@5e3aeb5 · GitHub) of ColorTypes.jl.

Consolidated benchmarks of every method from this thread after `]develop ColorTypes` and Julia restart (or presumably `]add ColorTypes` once 0.12 is released):

``````using RandomNumbers, BenchmarkTools, Random, Colors
rng = Random.default_rng()
rng_xor = RandomNumbers.Xorshifts.Xoroshiro128Star()

f1() = rand(RGB{Float32},1080,1980)

function f2()
M = Matrix{RGB{Float32}}(undef, 1080, 1980)
@inbounds for j in 1:1980
for i in 1:1080
M[i,j] = RGB(rand(), rand(), rand())
end
end
return M
end

function f2_rng(rng)
M = Matrix{RGB{Float32}}(undef, 1080, 1980)
@inbounds for j in 1:1980
for i in 1:1080
M[i,j] = RGB(rand(rng), rand(rng), rand(rng))
end
end
return M
end

function f3()
M = Matrix{RGB{Float32}}(undef, 1080, 1980)
@. M = RGB(rand(), rand(), rand())
return M
end

function f4(rng)
M = Matrix{RGB{Float32}}(undef, 1080, 1980)
@. M = RGB(rand(rng), rand(rng), rand(rng))
return M
end

@btime rand(RGB{Float32},1080,1980) seconds=1
# 9.539 ms (4 allocations: 24.47 MiB)

@btime rand(\$rng, RGB{Float32},1080,1980) seconds=1
# 9.704 ms (4 allocations: 24.47 MiB)

@btime rand(\$rng_xor, RGB{Float32},1080,1980) seconds=1
# 12.198 ms (4 allocations: 24.47 MiB)

@btime f1() seconds=1
# 9.622 ms (4 allocations: 24.47 MiB)

@btime f2() seconds=1
# 26.670 ms (2 allocations: 24.47 MiB)

@btime f2_rng(\$rng) seconds=1
# 11.479 ms (2 allocations: 24.47 MiB)

@btime f2_rng(\$rng_xor) seconds=1
# 11.054 ms (2 allocations: 24.47 MiB)

@btime f3() seconds=1
# 27.571 ms (2 allocations: 24.47 MiB)

@btime f2_rng(\$rng) seconds=1
# 11.446 ms (2 allocations: 24.47 MiB)

@btime f2_rng(\$rng_xor) seconds=1
# 11.022 ms (2 allocations: 24.47 MiB)
``````

How we got here:

1. Calls to `rand()` are thread safe by assigning each thread its own `MersenneTwister` which comes at a slight performance penalty at the time the thread’s generator is selected.
2. Vectorized `rand(1080, 1980)` calls look up the thread’s generator first with `default_rng()`, and pass that generator along so that the lookup happens only once.
3. `rand(RGB{Float32}, 1080, 1980)` used to be slow because `ColorTypes.jl` did not explicitly pass random number generators around internally, resulting in `1080*1980-1` unnecessary calls to `default_rng()`
5 Likes