# /// script
# requires-python = ">=3.10"
# ///
# Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT
# https://thedataengineering.com/lab-code/LICENSE.txt
"""False sharing: measure the cache line.

8 threads each increment their own counter 100 million times: counters packed
in one array, padded to 64 bytes, padded to 128 bytes (the line size on Apple
silicon), and counted in a local variable written once at the end; plus one
thread alone. The C program is compiled and run by this script (needs cc).
"""
import argparse, json, os, platform, subprocess, tempfile

p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
p.add_argument("--threads", type=int, default=8, help="Threads.")
p.add_argument("--iter", type=int, default=100_000_000, help="Increments per thread.")
p.add_argument("--repeats", type=int, default=5, help="Runs per variant; the median is reported.")
p.add_argument("--json", help="Also write the results to this JSON file.")
args = p.parse_args()

C = r"""
#include <pthread.h>
#include <stdio.h>
#include <time.h>
#ifndef T
#define T 8
#endif
#if defined(PAD)
struct { volatile long v; char pad[PAD - sizeof(long)]; } counts[T];
#define C(i) counts[i].v
#else
volatile long counts[T];
#define C(i) counts[i]
#endif
static void *worker(void *arg) {
    long id = (long)arg;
#ifdef LOCAL
    long local = 0;
    for (long i = 0; i < ITER; i++) { local++; __asm__ volatile("" : "+r"(local)); }   /* kept in a register */
    C(id) = local;
#else
    for (long i = 0; i < ITER; i++) C(id)++;
#endif
    return NULL;
}
int main(void) {
    pthread_t th[T]; struct timespec a, b;
    clock_gettime(CLOCK_MONOTONIC, &a);
    for (long i = 0; i < T; i++) pthread_create(&th[i], NULL, worker, (void *)i);
    for (int i = 0; i < T; i++) pthread_join(th[i], NULL);
    clock_gettime(CLOCK_MONOTONIC, &b);
    printf("%.3f\n", (b.tv_sec - a.tv_sec) + (b.tv_nsec - a.tv_nsec) / 1e9);
}
"""
d = tempfile.mkdtemp(); src = os.path.join(d, "lab.c"); open(src, "w").write(C)
def run(label, flags):
    exe = os.path.join(d, "lab_" + str(abs(hash(label))))
    subprocess.run(["cc", "-O1", "-pthread", src, "-o", exe, f"-DITER={args.iter}L", *flags], check=True)
    runs = sorted(float(subprocess.run([exe], capture_output=True, text=True, check=True).stdout) for _ in range(args.repeats))
    return {"variant": label, "seconds": runs[len(runs) // 2]}
rows = [run("1 thread alone", ["-DT=1"]),
        run(f"{args.threads} threads, packed counters", [f"-DT={args.threads}"]),
        run(f"{args.threads} threads, padded to 64 bytes", [f"-DT={args.threads}", "-DPAD=64"]),
        run(f"{args.threads} threads, padded to 128 bytes", [f"-DT={args.threads}", "-DPAD=128"]),
        run(f"{args.threads} threads, local counter", [f"-DT={args.threads}", "-DLOCAL"])]
for r in rows: print(f"{r['variant']:<34} {r['seconds']:6.2f} s")
print(f"CPU: {platform.machine()} {platform.processor()}")

if args.json:
    with open(args.json, "w") as f:
        json.dump({"tables": {"variants": rows, "summary": [{"packed_over_one": rows[1]["seconds"] / rows[0]["seconds"],
                   "pad64_over_one": rows[2]["seconds"] / rows[0]["seconds"], "pad128_over_one": rows[3]["seconds"] / rows[0]["seconds"]}]}}, f)
