Ideas

Monte Carlo Pi Approximations: C++, Julia and Python

Benchmarking execution speed and accuracy across C++, Julia and Python

My coding background began with C++ in middle school competitive programming before shifting to Python for astrophysics and cosmology projects due to its rich ecosystem (numpy, scipy, matplotlib, astropy). However, with the growing adoption of Julia in scientific computing and a desire to refresh my C++ knowledge, I set out to test how these three languages compare on a classic numerical benchmark: estimating π\pi using a Monte Carlo algorithm.

Monte Carlo algorithms are ideal for performance comparisons because they are straightforward to implement, CPU-bound, and scale effortlessly from millions to billions of iterations.

Setup

Monte Carlo methods estimate deterministic quantities using random sampling. Consider a unit square with an inscribed quadrant (or full unit circle centered at the origin). By generating uniform random coordinates (x,y)(x,y) within the unit square [0,1]×[0,1][0,1] \times [0,1], the probability of a point falling inside the circular arc x2+y21x^2 + y^2 \leq 1 is equal to the ratio of their areas:

A=14πr2=π4,A=r2=1A_\circ = \frac{1}{4} \pi r^2 = \frac{\pi}{4}, \quad A_\square = r^2 = 1     π=4AA4NinsideNtotal\implies \pi = 4 \frac{A_\circ}{A_\square} \approx 4 \frac{N_\text{inside}}{N_\text{total}}

By counting how many points satisfy x2+y21x^2 + y^2 \leq 1, we can approximate π\pi with increasing precision as NtotalN_\text{total} increases.

GIF Animations of the proces of 'dart-throwing' to approximate the value of Pi using Monte Carlo methods (illustrative)

Implementation

To ensure a rigorous comparison, each script uses high-quality random number generator and language optimisations:

LanguagePython 3.12C++ (g++ 13.3) Julia 1.12
Random Generatornumpy.random.randomstd::mt19937 (<random>)rand()
Execution ParadigmVectorised BatchingNative Scalar loop with -O3JIT Compilation Loop
Time Measurementtimehigh_resolution_clock@btime (BenchmarkTools)

Optimisation Strategies

  1. C++: Standard compiled scalar loops execute directly in CPU registers with small memory usage. Compiling with g++ -O3 enables maximum compiler optimisation and instruction-level parallelism.
  2. Julia: Julia relies on Just-In-Time (JIT) compilation. A warm-up run executes before timing to ensure LLVM compilation overhead is excluded from the benchmark.
  3. Python (NumPy): Pure Python loops suffer from interpreter overhead. Switching to NumPy vectorisation pushes the loops into C-level routines. To avoid allocating gigabytes of RAM when scaling up to N=1010N = 10^{10} points, computation is executed in memory-efficient batches of 10710^7 iterations.

All implementations and benchmarking automation scripts are available in the repository.

Results

Performance was evaluated across scale sizes ranging from N=106N=10^6 to N=1010N=10^{10} total points.

Execution Speed

Comparison plot of the execution time for C++, Julia and vectorised Python for 4 different samples sizes between 1E6 and 1E10 The first observation is that the plot is approximately linear, which means logtexec=logN\log t_\text{exec} = \log N. Though it is noteworthy, that for increasing NtotalN_\text{total} the absolute difference between languages becomes a 2x, 4x factor which should be considered for large runs.

Unexpectedly, Julia achieved the fastest execution time, even against C++. Julia’s JIT compiler optimised scalar loops into native assembly appears to be slightly more efficient for this use case than the closely matched C++ compiled code.

Using NumPy vectorisation reduced Python’s execution times from hundreds of seconds down to competitive numbers compared with previous script drafts. This demonstrates how array batching overcomes interpreter overhead in Python’s case.

Accuracy and Convergence

Accuracy of Pi estimates for C++, Julia and vectorised Python for 4 different samples sizes between 1E6 and 1E10 According to the Central Limit Theorem, the error in Monte Carlo integration scales as O(1/N)O(1/\sqrt{N}). All three languages demonstrated identical convergence rates, with the absolute error π^π|\hat{\pi} - \pi| decreasing reliably as sample size NN approached 101010^{10} points. Small variance between runs at identical NN values is purely a result of differing random number engine seeds.

Conclusions

While traditional Python interpreter loops are poorly suited for raw numerical loops, modern NumPy vectorisation makes Python competitive for array-based operations on relatively small Ntotal<107N_\text{total} < 10^7.

For maximum performance without writing C++ boilerplate, Julia provides an impressive balance: its JIT compiler generates machine code on par with optimised C++ while retaining the readable syntax of a high-level dynamically typed language.

This is my first attempt to a blog-style post on my website’s new ideas directory. If you have feedback, suggestions or any constructive comments, I encourage you to contact me. Thank you in advance!