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