2

I have a problem, I want to use rand() to get a random number between 0 and 6, but it always gives me 4 at each run, even when I call srand(time(NULL))

#include <stdlib.h>
#include <stdio.h>
#include <time.h>

int main(void)
{
    srand(time(NULL));

    int rd = rand() % 7;

    printf("%d\n", rd);
    return (0);
}

output is 4 at each run

Fayeure
  • 1,181
  • 9
  • 21
  • Try `srand(time(0));` – risingStark Mar 25 '21 at 22:09
  • 6
    Doesn't happen for me. Are you waiting more than 1 second between runs? – Nate Eldredge Mar 25 '21 at 22:10
  • @risingStark: That shouldn't matter. – Nate Eldredge Mar 25 '21 at 22:11
  • 1
    @NateEldredge Yes, I'm waiting 1 second, it's wierd maybe something is broken and I need to restart my computer? I'll try that and keep you informed – Fayeure Mar 25 '21 at 22:13
  • Unlikely, unless your computer is really broken. What OS, compiler, library, etc? – Nate Eldredge Mar 25 '21 at 22:13
  • Is it possible you're running a previous version of your program from before you added `srand(time(NULL));`? Did you remember to save your source file, recompile, etc? – Nate Eldredge Mar 25 '21 at 22:15
  • I'm on macOs 10.15.7, I tried to restart but nothing changed, I still get 4 everytime, it's wierd because I already used the rand function and it always worked since now... – Fayeure Mar 25 '21 at 22:17
  • yes I recompile everytime before trying but nothing changes – Fayeure Mar 25 '21 at 22:18
  • I did the same minimal reproductible exemple that is in the post and it does the same thing, so it doesn't come from my program – Fayeure Mar 25 '21 at 22:18
  • 1
    See if `time()` is working. `printf("%ld\n", (long) time(NULL));`. Always the same? – chux - Reinstate Monica Mar 25 '21 at 22:25
  • I would try to check the value that time(NULL) returned. If it is -1 or 0 or some other value that is returned all the time and doesn't change, then the problem is with the time() function, otherwise, the problem is with the srand() or rand() functions. – Orielno Mar 25 '21 at 22:26
  • `time(NULL)` is working, it gives me a different result each time, the problem seems to come from `srand` – Fayeure Mar 25 '21 at 22:35
  • Problem solved: I used `srandom` and `random` functions instead and it works, but I don't know why `rand` doesn't – Fayeure Mar 25 '21 at 22:41
  • The `% 7` is throwing away information that might be useful in diagnosing this. I'd be interested in seeing the output of something like this: `time_t now = time(NULL); srand(now); int r0 = rand(); int r1 = rand(); printf("%ld %d %d\n", (long)now, r0, r1);` (I know that some rand` implementations have serious problems, worse than the sample implementation in K&R). – Keith Thompson Mar 25 '21 at 23:19
  • 1
    @keith: Actually, the %7 is extremely relevant. As I note in my answer, the problem is that the FreeBSD code involves multiplying the low-order part of the seed by a multiple of 7, so only the high-order part is relevant to the computation modulo 7. So it's less bad at rolling 6-sided dice than 7-sided dice. – rici Mar 26 '21 at 00:21
  • @rici To be clear, I wasn't suggesting that the `% 7` isn't relevant. I've seen `rand()` implementations where the low-order bit consistently alternates 0 and 1, the low-order 2 bits repeat with a cycle of length 4, the low-order 3 bits have a cycle of 8, and so on. Which is why applying `% N` is a bad idea, especially if N is a power of 2. This problem, if I understand correctly, applies only to the *first* result returned after `srand()`. If you need to use `srand` and `rand` for some reason, section 13 of the [comp.lang.c FAQ](http://www.c-faq.com/) discusses some of the issues. – Keith Thompson Mar 26 '21 at 01:47
  • @KeithThompson: In a certain sense, the randomness improves after the first result, yes. It used to be common to recommend throwing away some number of PRNs after reseeding, but it turns out to be better to use a good seed and a good PRNG rather than bandaids. Although the MacOS/FreeBSD weirdness with %7 is interesting, the point I'd really like to get across is that seeding with `time(NULL)` is really a bad idea, and that all of us would be well-advised to stop producing example programs which do that. But I fear that message is never going to be well-received. – rici Mar 26 '21 at 02:14
  • @rici I'm not sure I entirely agree. If you have a decent implementation (at least as good as the sample code in K&R) and don't rely on the low-order bits, `srand(time(NULL)); ... rand()` is probably good enough *for a toy program*. And it has the virtue of being standard. Of course for anything beyond, say, a guessing game in an introductory course, `rand()` is not good enough -- and for anything within sniffing distance of cryptography it's as bad as a simile for something really really bad. (Yeah, I kind of fizzled out at the end there.) – Keith Thompson Mar 26 '21 at 05:09
  • @rici Fair enough. – Keith Thompson Mar 26 '21 at 08:30

2 Answers2

6

There are two fundamental problems with your code which, in combination, produce the curious result you're experiencing.

Almost anyone will warn you about the use of the rand() interface. Indeed, the Mac OS manpage itself starts with a warning:

$ man rand
NAME
     rand, srand, sranddev, rand_r -- bad random number generator

Yep, it's a bad random number generator. Bad random number generators can be hard to seed, among other problems.

But speaking of seeding, here's another issue, perhaps less discussed but nonetheless important: Do not use time(NULL) to seed your random number generator.

The linked answer goes into more detail about this, but the basic issue is simple: the value of time(NULL) changes infrequently (if frequently is measured in nanoseconds), and doesn't change much when it changes. So not only are you relying on the program to not be run very often (or at least less than once per second), you're also depending on the random number generator to produce radically different values from slightly different seeds. Perhaps a good random number generator would do that, but we've already established that rand() is a bad random number generator.

OK, that's all very general. The specific problem is somewhat interesting, at least for academic purposes (academic, since the practicial solution is always "use a better random number generator and seed it with a good random seed"). The precise problem here is that you're using rand() % 7.

That's a problem because what the Mac OS / FreeBSD implementation of rand() does is to multiply the seed by a multiple of 7. Because that product is reduced modulo 232 (which is not a multiple of 7), the value modulo 7 of the first random number produced by slowly incrementing seeds will eventually change, but it will have to wait until the amount of the overflow changes.

Here's a link to the code. The essence is in these three lines:

    hi = *ctx / 127773;
    lo = *ctx % 127773;
    x = 16807 * lo - 2836 * hi;

which, according to a comment, "compute[s] x = (7^5 * x) mod (2^31 - 1) without overflowing 31 bits." x is the value which will eventually be returned (modulo 232) and it is also the next seed. *ctx is the current seed.

16807 is, as the comment says, 75, which is obviously divisible by 7. And 2836 mod 7 is 1. So by the rules of modular arithmetic:

x mod 7 = (16807 * lo) mod 7 - (2836 * hi) mod 7
        =          0         -       hi mod 7   

That value only depends on hi, which is seed / 127773. So hi changes exactly once every 127773 ticks. Since the result of time(NULL) is in seconds, that's one change in 127773 seconds, which is about a day and a half. So if you ran your program once a day, you'd notice that the first random number is sometimes the same as the previous day and sometimes one less. But you're running it quite a bit more often than that, even if you wait a few seconds between runs, so you just see the same first random number every time. Eventually it will tick down and then you'll see a series of 3s instead of 4s.

rici
  • 234,347
  • 28
  • 237
  • 341
  • Thanks, I know `time(NULL)` is not efficient to generate a seed to `srand` but I just did that for the minimal reproductible example to show that the number isn't changing at all, but I don't use it in my projects, but now I understand why it wasn't working, but it is working nice with `random` and `srandom` functions, are they bad too or are they a better random generator when used correctly? – Fayeure Mar 28 '21 at 14:50
  • @fayeure: better, but still far from what's desired. But you still shouldn't use `time(NULL)` as a seed. It's unfortunate that there is no standard function like `sranddev` in C, but it's easy to write one. (Or just borrow the FreeBSD code.) – rici Mar 28 '21 at 15:14
  • @fayeure: despite `rand()`'s shortcomings, had you used `sranddev()` instead of `srand(time(NULL))`, you would not have seen the problem. So there is a causal link between seeding with `time(NULL)` and the problem you experienced. I'd be interested in seeing the code you used to seed before you decided to create a minimal example. To be sure, using `rand()` is always a bad idea, as well. – rici Mar 28 '21 at 15:21
  • Usually i'm using a `struct timespec` and call `clock_gettime` and then I use`tv_nsec` for the `srand` seed, but maybe in my program I used `time(NULL)` for testing and forgot to remove it, but seeing that the `time(NULL)` function worked I thought the problem came from `srand` only, but now I know it comes from both functions – Fayeure Mar 28 '21 at 15:25
  • @fayeure: /dev/urandom is better than any time-based solution, and it's easy to implement. Unfortunately it doesn't exist on Windows (although I believe Cygwin emulates it); Windows-specific APIs exist, the simplest of which is `rand_s`. Fwiw, I use a function based on those which also takes a command-line option which can be used to manually set the seed. If the option is not provided, a random seed is generated and logged; that makes it easy to reproduce and debug problems. – rici Mar 28 '21 at 15:44
  • Okay, but what is the difference between `/dev/random` and `/dev/urandom` ? – Fayeure Mar 28 '21 at 15:46
  • @fayeure: on a Mac, nothing. They are synonyms. On some unix-like systems, /dev/random will block if there is not enough entropy in the kernel's random source, while /dev/urandom will provide a "less random" result. So if you need a lot if random numbers, /dev/random might cause problems. (Also if you try to get a random number shortly after system startup.) These days, it mostly doesn't make a difference. – rici Mar 28 '21 at 15:59
0

As mentioned by @rici, the problem is caused by the poor implementation of rand(). The man page for srand() recommends using arc4random() instead. Alternatively, you could try seeding with a value taken directly from /dev/urandom as follows:

#include <stdlib.h>
#include <stdio.h>

int main(void)
{
    int seed;
    FILE *f = fopen("/dev/urandom", "r");
    fread(&seed, sizeof(int), 1, f);
    srand(seed);
    fclose(f);
    /* Should be a lot more unpredictable: */
    printf("%d\n", rand() % 7);
    return (0);
}
r3mainer
  • 23,981
  • 3
  • 51
  • 88