Estimating Pi with Monte Carlo Simulation

Jul 30. 2013

About the Problem

Estimating Pi is the "Hello World" of Monte Carlo simulations. It is often used as the first example of GPGPU programming, as the problem is trivially parallelizable (as are most monte carlo problems, by their nature).

A lot of people don't understand how we can possibly compute pi, and so the problem is a fun demonstration. It's also a pretty good interview question!

The Setup

Given a perfect random number sampler, can we use it to somehow estimate the value of pi?

We first start by setting up our problem, and visualizing what it might look like. If we can set our sample to return (x,y) samples in the range of -radius...+radius, then, after performing a few samples, we might have a situation that looks something like this:

Drawing

How do we use those random samples to compute Pi? Well, we can infer that the ratio between samples landing inside the circle vs. the total number of samples is equal to the ratio between the areas of the two:

C_inside / C_total = radius^2 * pi / (2 * radius)^2 (approx)

Solving for pi, we end up with:

pi = 4 * C_inside / C_total (approx)

We've omitted one important detail: How do we check if a sample is within the circle? That turns out to be easy, if you paid attention in 6th grade math class. Pythagoras gave us the answer many years ago:

Drawing

In order to check whether the sample is inside a circle of radius r, we compute:

Sqrt(x*x + y*y) < r

We now have all the building blocks required in order to write a short program to simulate this 10 million times.

public static void Main(string[] args)
{
    double radius = 1.0;
    int cInside = 0;
    int cTotal = 10000000;
    var rand = new Random();

    for (int i = 0; i < cTotal; i++)
    {
        var x = rand.NextDouble();
        var y = rand.NextDouble();
        var isInside = Math.Sqrt(x*x + y * y) < radius;
        if (isInside)
            cInside++;
    }

    var pi = 4 * cInside / (double)cTotal;
    Console.WriteLine("Pi is approx {0:0.0000}", pi);
}

The output:

Pi is approx 3.1419

Well, that was easy! Granted, it's not too accurate, but that does give 4 significant figures for only 291 ms of runtime (test with an i7 4930K).

But wait, you mentioned GPGPU and parallelization! This is single threaded, how can we improve it?

Let's try using PLINQ to parallelize it, and re-write the program as such:

public static void Main(string[] args)
{
    double radius = 1.0;
    int cInside = 0;
    int cTotal = 10000000;
    var parallelCount = Environment.ProcessorCount;

    cInside = Enumerable.Range(0, parallelCount).AsParallel().Select(_ =>
    {
        var cInsideLocal = 0;
        var rand = new Random();

        for (int i = 0; i < cTotal; i++)
        {
            var x = rand.NextDouble();
            var y = rand.NextDouble();
            var isInside = Math.Sqrt(x*x + y * y) < radius;
            if (isInside)
                cInsideLocal++;
        }
        return cInsideLocal;
    }).Sum();

    var pi = 4 * cInside / (double)(cTotal * parallelCount);
    Console.WriteLine("Pi is approx {0:0.0000}", pi);
}

On an i7 Extreme with 6 logical cores it can run 12 threads with hyperthreading in parallel. This code takes 460ms to execute (about 60% longer overall), but it is in fact computing 12x more samples than before, as the operating system is reporting Environment.ProcessorCount = 12. This means the program is now about 7.5x faster than before.

Some Notes:

I'll be posting a CUDA version of this code soon.