Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Questions & Answers


May 1996/Questions & Answers

Random number generation remains one of the most poorly understood topics in programming. This month Pete clears up some questions about rand() raised in one of his previous columns.


Q

I was reading your magazine and came across your discussion on rand. You provide the following example:

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

int roll(void) {
      const int min_val = 1;
      const int max_val = 6;
      const int range = max_val - min_val + 1;
      return rand() % range + min_val;
}

int main(void) {
      int n;
      for (n=0; n<100; n++)
      printf("%2d:%d\n",n+1,roll());
      return 0;
}


Then you go and tell us this doesn't work because roll is distorted towards low values. Did I miss something here? I've tried this on the new Borland Win95 compiler several times doing several 1000 rolls and the results came back very evenly distributed. You blew this one!
— Gerald Wright

A

I've received several messages saying the same sort of thing as this one. It looks like I didn't do a good enough job of explaining the underlying problem, so I'll try it again. But instead of looking at a small range, let's look at a much bigger one.

rand returns a number between 0 and RAND_MAX, inclusive. To see the problem in its simplest form, consider what happens when we try to generate random numbers between 0 and RAND_MAX-1, inclusive:


unsigned random()
{
      return rand() % RAND_MAX;
}


Whenever rand returns a number between 0 and RAND_MAX-1 random returns that value. When rand returns the number RAND_MAX, random returns 0. That is, the function random will return 0 when rand returns 0 and when rand returns RAND_MAX. random will return 1 when rand returns 1; it will return 2 when rand returns 2; and so on. The result is that random will produce twice as many 0's as any other number, because there are twice as many values returned by rand that end up producing 0.

Similarly, if we try to get numbers between 0 and RAND_MAX-2, our function looks like this:

unsigned random()
{
      return rand() % (RAND_MAX-1);
}


The same sort of analysis tells us that random will return 0 whenever rand returns 0 or RAND_MAX-1; it will return 1 whenever rand returns 1 or RAND_MAX; it will return 2 whenever rand returns 2; and so on. There are two return values of rand that will produce 0, two return values that will produce 1, and only one value that will produce 2, 3, 4, or any other number up to RAND_MAX-2. So this version of random will return 0 and 1 twice as often as it returns any other value.

The problem we see here isn't some deep property of rand or random numbers. It's simply a counting problem, like trying to put 32,768 marbles into 32,767 bins. No matter how we distribute them, there is going to be at least one bin that gets more than one marble. The same problem occurs when we try to map the range of numbers returned by rand to the range of numbers that we need for our problem: unless the ranges fit together evenly we'll get some numbers more often than others. Those extra marbles have to go somewhere.

In practice this problem can be hard to see. Let's continue with the example that started this off: trying to generate numbers in the range 0 to 5. With Borland C++ 5.0, RAND_MAX is 32767, so when we call rand we get back numbers in the range 0 to 32767. If we divide the resulting number by 6 and take the remainder, we'll get numbers in the range 0 to 5. Let's look at which numbers returned by rand map to these values:

rand()                    random()

0,6,12,.,32760,32766      0
1,7,13,.,32761,32767      1
2,8,14,.,32762            2
3,9,15,.,32763            3
4,10,16,.,32764           4
5,11,17,.,32765           5


There are 5,462 different values returned by rand that result in random returning 0. Since rand returns 32,768 possible values, this means that the probability of getting a 0 from random is 5462/32768, or 16.6687%. There are 5461 different values that result in random returning 5, so the probability of getting a 5 from random is 5461/32768, or 16.6656%. The difference between the probability of getting a 0 and the probability of getting a 5 is so small that you won't notice it without checking a very large number of results. Nevertheless, it is there. The bias that we see here wouldn't make users return your adventure game, but in other applications it could make a difference.

Q

I just got a chance to read your discussion about mapping rand's output into a different integer range and thought that there are a couple of issues that you might like to account for.

The most important point is that the lower bits of the output from the usual (linear congruential) random number generators are the least "random." That is, patterns in the lower bits are common. Hence, the output from the routine roll in your discussion is not surprising. Also, it is avoidable by relying on the upper bits to determine the integer returned.

Second, there is a payoff to thinking about mapping ranges of values in a fairly general way and then applying the thoughts in the particular circumstances.

The first point means that getting a fraction on the interval from zero to one and then converting to an integer is far preferable to using the remainder. While using an intermediate floating-point number is slower (at least on the machines that I use), it will typically not have an output of predictable sequences. Given the strategy of using a fraction and then converting to integral values, it is fairly straightforward to think about converting ranges of values.

The output of the Borland DOS and OS/2 compilers ranges from 0 to RAND_MAX, which can be written [0, RAND_MAX]. The following line generates a fraction on the interval [0, RAND_MAX/(RAND_MAX+1)]:

rand()/((double)RAND_MAX+1.0)

where the cast should avoid possible integer overflow. [The cast is not necessary: the compiler will convert to double because 1.0 is a double. — pjb] For most purposes, multiplying the resulting fraction by the maximum value desired, max_val in your routines, truncating to an integer, and then adding one will do the trick, producing only nonobvious patterns. (I say this in this convoluted way because all random number generators are deterministic — perfectly predictable given the initial conditions and the rule generating the "random" numbers.) This will work fine as long as max_val is small relative to RAND_MAX+1 or RAND_MAX+1 is perfectly divisible by max_val+1. If max_val is large relative to RAND_MAX+1 and RAND_MAX+1 is not perfectly divisible by max_val+1, then more care is necessary to make sure that the relative proportions of the different values are the same. (As you do, I assume that max_val is less than RAND_MAX.) This is where throwing away values could come into play, although there are other possibilities.

I enjoy your column very much and would not bother you with this, but no one can know everything and it is a well-established rule to use the upper bits of congruential generators to generate numbers other than the integers produced by the built-in routines.

— Jerry Dwyer

A

You're right that I should not have neglected the well- known problem with the low bits of values produced by linear congruential generators. Let's look in a bit more detail at what we mean when we say that we want rand to generate a good pseudo-random sequence. Along the way we'll get a sense of how well the implementation in BC++ 5.0 does, because that's the compiler I've used to generate the results that we'll see here.

First, a pseudo-random number generator needs to produce every number in its output range with equal probability. That's easy to measure: just run the random number generator many times and count the number of occurrences of each number. The following program scales the output of rand down so it generates numbers greater than or equal to 0 and less than 10. That makes it easy to count how many times each number occurs, and to compare them by eye.

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

int main()
{
      int i;
      int data[10];
      srand(time(NULL));
      for( i = 0; i < 10; i++ )
   data[i] = 0;
      for( i = 0; i < 1000000000; i++ )
   data[rand()%10]++;
      for( i = 0; i < 10; i++ )
   printf( "count[%d] = %d\n", i, data[i] );
      return 0;
}


The results show no glaring discrepancies from the even distribution we would hope to get:

count[0] = 100006000
count[1] = 100018371
count[2] = 100006464
count[3] = 100001979
count[4] = 100009869
count[5] = 99998331
count[6] = 99999846
count[7] = 100007726
count[8] = 99977846
count[9] = 99973568


That is, we would expect each number to occur 100,000,000 times. The actual results are quite close: none of the counts is off by more than a few tenths of a percent. For an adventure game, that's certainly good enough.

Knowing that we can produce values that are evenly spread over the range we are interested in is a good first step. However, in itself it's not enough. A good pseudo-random number generator also avoids short-term patterns. For example, a generator that always produces a 2 immediately after it has produced a 1 is not a good pseudo-random number generator, even if it covers its full range reasonably well. It is too predictable. To make this notion a bit more formal, in such a generator the sequence { 1,2 } would be much more common than the sequence { 1,3 }. Stated the other way around, a good pseudo-random number generator will produce every sequence of a given length with equal frequency. That's where linear congruential generators are said to fall down: they produce short-term patterns that are fairly easy to see. The following program checks the frequency of all possible sequences of two numbers:


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

#define NUM_VALS 1000000000

int main()
{
   int i,j;
   int count[10][10];
   int lastnum;
   srand(time(NULL));
   for( i = 0; i < 10; i++ )
      for( j = 0; j < 10; j++ )
         count[i][j] = 0;
   lastnum = rand()%10;
   for( i=0; i < NUM_VALS; i++ )
      {
      int curnum = rand()%10;
      count[lastnum][curnum]++;
      lastnum = curnum;
      }
   for( i = 0; i < 10; i++ )
      {
      for( j = 0; j < 10; j++ )
        printf( "%6.3f",
      count[i][j]*100/(double)NUM_VALS );
      printf( "\n" );
      }
   return 0;
}


When this program has completed the count array holds the number of two-number sequences whose first number is the value of the first array index and whose second number is the value of the second array index. If we have a good pseudo-random number generator these counts should be very close to equal. Rather than show the raw data, the program displays the frequency of each two-number sequence as a percentage of the total number of two-number sequences in the output. This produces the results shown in Table 1. There are one hundred possible two number sequences, so each one should occur one percent of the time. What we see here is that we're very close to one percent. None of the values is off by more than one part in one thousand. By this test, rand does pretty well in its low digits.

It is also said that linear congruential generators sometimes alternate odd and even numbers. If that were so, the previous table would show a higher percentage of sequences where the first number is odd and the second even, and where the first number is even and the second odd. Again, the data does not show this, so this particular random number generator does not seem to suffer from this problem.

If you need to carefully test your random number generator you need to perform tests on longer sequences than these. The bookkeeping involved becomes a bit harder, but the principle is the same: count the number of distinct sequences and look for imbalances. If you find any, you should be suspicious of your random number generator. If you don't find any, be careful about concluding that your random number generator works well. You may not be looking hard enough.

If your random number generator does have problems in its low digits, you're right that using floating-point computations to map to the desired range will help reduce the effect of that bias. Note, however, that it does not help with the problem mentioned in the preceding letter: if you're trying to put 32,768 marbles into 32,767 bins, counting in floating-point doesn't make the extra one go away. It just changes where the extra value ends up, putting it in the middle instead of at the beginning.

Q

The answer to the question about private virtual functions ("Questions & Answers," November 1995) was incorrect; the sample code won't compile. Member access is a compile-time check, whereas the virtual function mechanism is a run-time feature. Below is the sample code, compiled with three different C++ compilers, with error messages.

class Base {
    virtual void func();
};

class Derived : public Base {
    public:
    void func();
};

void sample()
{
    Base *bp = new Derived;
    //calls Derived::func?
    //doesn't compile!
    bp->func();

}


— Gerard C. Weatherby

A

AI left out the error messages you got, to avoid further embarrassment. You're absolutely right: that code is incorrect. It should not compile, because func is private in Base. I usually compile the code that I include in this column, but this fragment was so simple that I just knew I couldn't get it wrong. We recently lost half a day of testing when someone made a simple change to some code that didn't need to be changed and broke our debugger. It took that long to figure out what had gone wrong, back out the change, rebuild, and redistribute the code to our testing staff. Don't ever assume that writing code is simple. Our business is precision, and precision requires close attention. If you take shortcuts you'll make mistakes.

Pete Becker is Senior Development Manager for C++ Quality Assurance at Borland International. He has been involved with C++ as a developer and manager at Borland for the past six years, and is Borland's principal representative to the ANSI/ISO C++ standardization committee.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.