2

Say you want to generate a random number between 1 and 1 billion:

rand(1..1_000_000_000)

Will Ruby create an array from that range every time you call this line of code?

Rubocop suggests this approach over rand(1_000_000_000)+1 but it seems there's potential for pain.

Ruby's docs say this:

# When +max+ is a Range, +rand+ returns a random number where
# range.member?(number) == true.

Where +max+ is the argument passed to rand, but it doesn't say how it gets the number argument. I'm also not sure if calling .member? on a range is performant.

Any ideas?

I can use benchmark but still curious about the inner workings here.

Ryan Clark
  • 764
  • 1
  • 8
  • 29
  • 3
    If the documentation mentions `Range` specifically, you can be pretty sure it’s efficient. You can also just try with more memory than you have: `rand(1..2**64)` – Ry- Dec 30 '17 at 04:39
  • Can you expand? I know they're lazy loaded for enumerating but why else are they efficient? Like, why in this case? – Ryan Clark Dec 30 '17 at 09:04
  • You asked whether or not an array is created. As the other Ryan said, you can test that easily, by asking for a random number in a range that is so large that allocating an array for it would consume more than your available memory. If the code works, then no array is created, if the code starts thrashing the swap file, or raises an out of memory exception, then the intermediate array is created. – Jörg W Mittag Dec 30 '17 at 10:23
  • Calling `member?` on a range is constant, it is just `left <= i && i <= right`. But that is irrelevant, because `member?` is never going to be called anyway. The documentation only says that it will return a number that is inside the range, so that *if you **were** to call `member?`, it **would** return `true`*. – Jörg W Mittag Dec 30 '17 at 10:25
  • Since you seem to be satisfied that it’s possible to compute `rand(1_000_000_000) + 1` without creating an array: `rand(a..b)` can just evaluate `rand(b - a + 1) + a`. – Ry- Dec 30 '17 at 10:27
  • I don't understand how creating an array would help, as you would then need to compute a random index, which is the same problem. – Cary Swoveland Dec 30 '17 at 20:51
  • Definitely don't think it would help, just wasn't sure if it would. Seems like the answer is that it never will and Rubocop's approach is safe. I ran both styles with Benchmark and they're close enough performance-wise that it doesn't make a difference. Thanks for the comments. Happy to accept an answer that sums this stuff up. – Ryan Clark Dec 30 '17 at 22:30

1 Answers1

4

No, Ruby will not create an array from that range, unless you explicitly call the .to_a method on the Range object. In fact, rand() doesn't work on arrays - .sample is the method to use for returning a random element from an array.

The Range class includes Enumerable so you get Enumerable's iteration methods without having to convert the range into an array. The lower and upper limits for a Range are (-Float::INFINITY..Float::INFINITY), although that will result in a Numerical argument out of domain error if you pass it into rand.

As for .member?, that method simply calls a C function called range_cover that calls another one called r_cover_p which checks if a value is between two numbers or strings.

To test the difference in speed between passing a range to rand and calling sample on an array, you can perform the following test:

require 'benchmark'

puts Benchmark.measure { rand(0..10_000_000) }
=> 0.000000   0.000000   0.000000 (  0.000009)

puts Benchmark.measure { (0..10_000_000).to_a.sample }
=> 0.300000   0.030000   0.330000 (  0.347752)

As you can see in the first example, passing in a range as a parameter to rand is extremely rapid.

Contrarily, calling .to_a.sample on a range is rather slow. This is due to the array creation process which requires allocating the appropriate data into memory. The .sample method should be relatively fast as it simply passes a random and unique index into the array and returns that element.

To check out the code for range have a look here.

DaniG2k
  • 4,772
  • 36
  • 77