Random numbers

rand returns a pseudo-random integer between 0 and RAND_MAX, and srand sets the starting point of the sequence. The numbers are not random in the cryptographic sense, but they are repeatable, which is what a simulation or a test needs. Reproducibility is the property the whole lesson builds on.

rand with a seed

The remainder after division by six maps the range to 0..5, and adding one gives a die. The seed 42 is fixed, so the six values are the same on every run and on every machine that uses the same C library. A different seed starts a different sequence.

#include <stdio.h> #include <stdlib.h> int main(void) { srand(42); for (int i = 0; i < 6; i++) { printf("%d ", rand() % 6 + 1); } printf("\n"); return 0; }

gcc -Wall -Wextra dice.c -o dice
./dice

3 2 5 1 3 2

The values look random and are entirely determined by the seed and the library. The C standard does not define the algorithm behind rand, so the same program on another implementation may print a different sequence; within one build it never changes.

The same seed, the same sequence

Calling srand(7) a second time resets the sequence to its beginning, so the next three values repeat exactly. That is the feature a test uses: a failure with seed 7 can be reproduced later, and a bug that only appears with one seed is found by replaying it.

#include <stdio.h> #include <stdlib.h> int main(void) { srand(7); printf("%d %d %d\n", rand(), rand(), rand()); srand(7); printf("%d %d %d\n", rand(), rand(), rand()); return 0; }

gcc -Wall -Wextra repeat.c -o repeat
./repeat

23462604 1699202044 150330503
23462604 1699202044 150330503

The two lines are identical because the seed is the whole state of the generator. A program that wants a different sequence on every run seeds once from the clock, the mistake at the end shows the wrong way to do it. The values are large; the generator fills the whole RAND_MAX range.

A range and a double

Dividing by RAND_MAX maps the integer range to 0..1, which is the usual source for a fraction. The casts to double before the division matter: integer division would give only zeroes. The example prints three values with six decimals.

#include <stdio.h> #include <stdlib.h> int main(void) { srand(123); for (int i = 0; i < 3; i++) { double value = (double) rand() / (double) RAND_MAX; printf("%.6f\n", value); } return 0; }

gcc -Wall -Wextra unit.c -o unit
./unit

0.090063
0.085460
0.003110

The values stay below one unless rand returns RAND_MAX exactly, so a range that must exclude one is safe. For a range [a, b) the formula is a + (b - a) * value; the modulo method of the dice is simpler but slightly biased, as the last section explains.

Counting the buckets

A generator is judged by its distribution. Sixty thousand throws into six buckets should give about ten thousand each, and the table below is close. The counts are deterministic for the seed, so the same program can be run as a regression test of the generator itself.

#include <stdio.h> #include <stdlib.h> int main(void) { int buckets[6] = {0}; srand(99); for (int i = 0; i < 60000; i++) { buckets[rand() % 6]++; } for (int i = 0; i < 6; i++) { printf("%d: %d\n", i + 1, buckets[i]); } return 0; }

gcc -Wall -Wextra counts.c -o counts
./counts

1: 10092
2: 10117
3: 9842
4: 10095
5: 9978
6: 9876

The largest bucket is about 1.2 percent above the average and the smallest about 1.2 percent below, which is normal for sixty thousand draws. The difference would shrink with more throws, and a table that stays lopsided with a million draws points at a broken generator rather than at bad luck.

Reseeding in a loop

time has a resolution of one second, so seeding inside a loop restarts the sequence with the same seed many times. The program prints the same number five times, which is the opposite of randomness:

#include <stdio.h> #include <stdlib.h> #include <time.h> int main(void) { for (int i = 0; i < 5; i++) { srand((unsigned int) time(NULL)); printf("%d ", rand() % 100); } printf("\n"); return 0; }

gcc -Wall -Wextra reseed.c -o reseed
./reseed

87 87 87 87 87

The seed is the same within the second, so every iteration starts from the same state. Seed once before the loop, not inside it. The second trap is the modulo bias: rand() % 6 is slightly uneven because RAND_MAX + 1 is not a multiple of six, so the first few remainders get one extra value; for a game the bias is invisible, for a simulation it is not.

Exercises

  1. Shuffle an array of ten integers with the Fisher-Yates algorithm and a fixed seed, and print the result twice to show it is repeatable.
  2. Generate a random double in the range -1 to 1 and count how many land below zero in a million draws.
  3. Seed from time(NULL) once, draw ten values, and run the program twice in different seconds to compare the sequences.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What does srand control?

    Answer

    The starting point of the rand sequence

    The seed sets the generator state, so the same seed produces the same sequence again.

  2. How is a fraction in the range 0..1 built from rand?

    Answer

    Divide a cast rand value by RAND_MAX

    The integer is converted to double before the division, so the result has a fractional part.

  3. Why is rand() % 6 slightly biased?

    Answer

    RAND_MAX + 1 is not a multiple of six

    The leftover values at the top of the range give a few remainders one extra chance, which matters in a simulation.

  4. Why should srand not be called inside a loop?

    Answer

    The seed may be the same within one second

    time has one-second resolution, so repeated seeding restarts the same sequence and prints repeated values.

  5. What six values does this program print for seed 42? #include <stdio.h> #include <stdlib.h> int main(void){srand(42);for(int i=0;i<6;i++)printf("%d ",rand()%6+1);printf("\n");return 0;}

    Answer

    3 2 5 1 3 2

    The fixed seed and the library algorithm determine the sequence, which is those six values here.

  6. What does this program print? #include <stdio.h> #include <stdlib.h> int main(void){srand(7);printf("%d %d %d\n",rand(),rand(),rand());srand(7);printf("%d %d %d\n",rand(),rand(),rand());return 0;}

    Answer

    23462604 1699202044 150330503 / 23462604 1699202044 150330503

    Reseeding resets the state, so the next three values are exactly the same as the first three.

  7. What three fractions does this program print for seed 123? #include <stdio.h> #include <stdlib.h> int main(void){srand(123);for(int i=0;i<3;i++)printf("%.6f\n",(double)rand()/(double)RAND_MAX);return 0;}

    Answer

    0.090063 / 0.085460 / 0.003110

    The fixed seed and six-decimal format produce those values, all below one.

  8. How close are the six buckets after sixty thousand throws?

    Answer

    Each is near ten thousand, within about 1.2 percent

    The counts are close to the average, with normal sampling variation for that many draws.

  9. What does reseeding inside the loop print?

    Answer

    The same number five times

    Every iteration starts from the same clock seed, so the first rand value repeats.

  10. What is the cause of the modulo bias?

    Answer

    The range size does not divide RAND_MAX + 1

    When the generator range is not a multiple of the modulus, some remainders receive one extra value.

The code on this page was run with gcc (Alpine 15.2.0) 15.2.0 on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev