Skip to content

Threading Benchmark

Monte Carlo Pi estimation benchmark demonstrating GIL release and OpenMP parallelism. Declares free_threading: true for Python 3.14t.

Threading Bench

Interface

# Python dict format equivalent of mc_pi.c2py
{
    "module": "mcpimod",
    "source": ["mc_pi.c"],
    "free_threading": True,
    "functions": [
        {
            "py_sig": "mc_pi(n: int, seed: int = 0) -> int",
            "gil_release": True,
            "c_overloads": [
                {
                    "sig": "mc_pi_serial(int n, int seed) -> int",
                    "map": {
                        "n": "n",
                        "seed": "seed",
                    },
                },
            ],
        },
        {
            "py_sig": "mc_pi_omp(n: int, seed: int = 0) -> int",
            "c_overloads": [
                {
                    "sig": "mc_pi_omp(int n, int seed) -> int",
                    "map": {
                        "n": "n",
                        "seed": "seed",
                    },
                },
            ],
        },
    ],
}

C Source

#include <stdint.h>

/* xorshift64 state */
typedef struct { uint64_t s; } xrs128_t;

static inline uint64_t xrs128_next(xrs128_t *st) {
    uint64_t x = st->s;
    x ^= x << 13;
    x ^= x >> 7;
    x ^= x << 17;
    st->s = x;
    return x;
}

static inline double xrs128_double(xrs128_t *st) {
    return (double)(xrs128_next(st) >> 11) * 0x1.0p-53;
}

static void xrs128_seed(xrs128_t *st, unsigned int seed) {
    st->s = (uint64_t)(seed + 1) * 0x9E3779B97F4A7C15ULL;
    (void)xrs128_next(st);
}

int mc_pi_serial(int n, int seed) {
    int inside = 0;
    int i;
    xrs128_t rng;
    xrs128_seed(&rng, (unsigned int)seed);

    for (i = 0; i < n; i++) {
        double x = xrs128_double(&rng);
        double y = xrs128_double(&rng);
        if (x * x + y * y <= 1.0)
            inside++;
    }
    return inside;
}

#ifdef _OPENMP
#include <omp.h>

int mc_pi_omp(int n, int seed) {
    int inside = 0;
#pragma omp parallel reduction(+ : inside)
    {
        int tid = omp_get_thread_num();
        xrs128_t rng;
        xrs128_seed(&rng, (unsigned int)(seed + tid * 7919));

        int i;
#pragma omp for
        for (i = 0; i < n; i++) {
            double x = xrs128_double(&rng);
            double y = xrs128_double(&rng);
            if (x * x + y * y <= 1.0)
                inside++;
        }
    }
    return inside;
}

int mc_pi_has_omp(void) {
    return 1;
}

#else

int mc_pi_omp(int n, int seed) {
    return mc_pi_serial(n, seed);
}

int mc_pi_has_omp(void) {
    return 0;
}
#endif

Build

$ c2py23 mc_pi.c2py -o mcpimod_wrapper.c

Compile:

$ cc -shared -fPIC c2py23/runtime/c2py_runtime.c mcpimod_wrapper.c mc_pi.c -I c2py23/runtime -o mcpimod.so -ldl -lm

See docs/building for cmake, meson, and setuptools options.

Run

$ python bench_mc_pi.py
ERROR: mcpimod.so not found. Build it first:
  cd /home/runner/work/c2py23/c2py23/examples/threading_bench && make

How It Works

mc_pi releases the GIL (gil_release: true) during C computation, allowing other Python threads to run. The C function generates random points in a unit square and counts how many fall inside the unit circle.

mc_pi_omp uses OpenMP #pragma omp parallel for for multi-core scaling. It is built with -fopenmp and linked against libgomp.

Both functions take an optional seed for deterministic runs (seed: int = 0).

free_threading: true at module level declares this module safe for Python 3.14t true parallelism (no GIL re-enablement).