gota・滴
Educational · Polyglot · Copy, don't depend

gota・滴

Cross-language throughput benchmarks, drop by drop.

The same operation, implemented in eight languages, measured the same way so the numbers actually compare. gota is a measurement protocol, a generic Python orchestrator, a runner template per language, and a self-contained HTML report. Copy it into your project; the protocol is the product, the code is small enough to re-type.

Eight languages · one JSON line per benchmark · no fabricated numbers

What a micro-benchmark is

A micro-benchmark isolates one small operation — a hash, a parse, a buffer copy — and runs it in a tight loop to measure how fast that single code path goes. It deliberately leaves out everything around it: no network, no disk, no real workload. That isolation is the whole point. It is what makes one operation's speed a clean, comparable number — here, the same operation across eight languages.

Approach What it measuresThe question it answersExample tools
Micro-benchmark One small operation, in isolation, loopedHow fast is this specific code path?gota; also criterion, JMH, Google Benchmark
Macro / application A whole program under a realistic workloadHow fast is the system, end to end?wrk, JMeter, k6, load tests
Profiling A real run, sampled to find hot spotsWhere is the time actually spent?perf, Instruments, flame graphs

These answer different questions and are complements, not rivals: a micro-benchmark tells you which implementation of an operation is faster, a macro benchmark tells you whether the whole system is fast enough, and a profiler tells you where to look. gota lives in the first row.

gota sits in that first row alongside criterion, JMH, and Google Benchmark, but it is a different kind of thing. Each of those is one language and a dependency you install — criterion in Rust, JMH in Java, Google Benchmark in C++. gota is the opposite: one ~20-line protocol you copy, not a package you depend on, spanning all eight languages so their results land in the same table. The numbers compare across languages, which a single-language harness cannot do.

One more axis: throughput, not latency. Even within micro-benchmarking, gota measures throughput — the peak rate (MB/s) a loop sustains, the unimpeded speed, which is what you want for comparing implementations. For the latency distribution of a single call (mean, p99, outlier rejection) reach for criterion or JMH; for whole-program performance, a macro benchmark. gota answers a narrower question on purpose, and the protocol below is how it keeps that one answer honest and comparable.

Three layers, one contract

A runner is usable on its own; the orchestrator is just the conductor that runs them together; the report is a pure consumer of the JSON. You can stop at any layer. Between the first two sits the protocol every runner obeys.

Measure. One native runner per language. Copy gota.<lang> as-is; it runs the peak-of-batches timing loop and prints one JSON line, {"impl","bench","mbps","mbps_median","iters"}, per benchmark.
Orchestrate. Python, once, across every language. harness.py builds and runs each runner under identical parameters, collects their JSON, records machine, date, git, and toolchain-version provenance, and writes results.json + a Markdown table.
Report. Optional and generic over the format. report.py turns any results.json into a standalone, sortable HTML viewer with a file picker, metric-aware units (MB/s or ops/sec), and baseline comparison.

Eight languages, one protocol

Two files per language: gota.* is the harness you copy untouched; runner.* is your code, where one seam (run(impl, register) then bench(name, op)) plugs an operation into the timing loop. Each runner is independently runnable.

Python

The orchestrator's own language and the simplest runner. No build step; run it straight.

gota.py · runner.py

Rust

A single-file rustc build, no Cargo project. Closures plug the op straight in.

gota.rs · runner.rs

C

The harness is a header plus one translation unit; the op is a function pointer with a void* ctx.

gota.h · gota.c · runner.c

Go

A tiny module with the harness in its own package. go build, then run.

go.mod · gota/gota.go · runner.go

Java

The op is a functional interface; javac the two files and run Runner.

Gota.java · Runner.java

Zig

Targets Zig 0.16 (std.process.Init, std.Io.Clock). Built ReleaseFast.

gota.zig · runner.zig

TypeScript

Runs under Node's type stripping (--experimental-strip-types), no transpile step.

gota.ts · runner.ts

Haskell

Lazy and pure; the op threads a forced accumulator so the work can't be deferred or shared. GHC, stdlib only.

Gota.hs · runner.hs

Language Harness (copy as-is)Your op (one seam)Build & run
Python gota.pyrunner.pypython3 runner.py 65536 0.2 0.4
Rust gota.rsrunner.rsrustc -O runner.rs -o runner
C gota.h + gota.crunner.ccc -std=c17 -O2 runner.c gota.c -o runner
Go gota/gota.gorunner.gogo build -o runner .
Java Gota.javaRunner.javajavac *.java && java Runner ...
Zig gota.zigrunner.zigzig build-exe runner.zig -O ReleaseFast
TypeScript gota.tsrunner.tsnode --experimental-strip-types runner.ts ...
Haskell Gota.hsrunner.hsghc -O2 runner.hs -o runner

Python is tinted because it also hosts the orchestrator (harness.py + your run.py). Every other row is just a runner. Full build notes live in each templates/<lang>/README.md.

See a live report

The bundled example — FNV-1a across all eight languages — rendered by report.py into a standalone, sortable HTML viewer. One toggle in the report flips the same measurement between byte throughput (MB/s) and operations per second (one op = hashing the whole 64 KiB buffer): same numbers, same ranking, two units.

A self-contained page with a file picker, so you can drop any other results.json in and re-render. Generated from the committed example data, never hand-edited.

The protocol is the product

Every runner obeys the same recipe, so numbers compare even though each runner is native to its language. This is the same discipline a multi-port project uses to keep ports in sync; here the ports are the timing harnesses.

  • Same three CLI args — buffer bytes, warmup seconds, measure seconds — passed identically to every runner.
  • Peak of batches, with a stability check. Warm up the JIT, grow a batch until it runs ≥ 100ms, then report the fastest batch's MB/s next to the median of all batches — a close gap means a clean run, a wide one means the peak is noisy. The clock is read only at batch boundaries, never around one tiny op.
  • One JSON line per benchmark on stdout ({"impl","bench","mbps","mbps_median","iters"}) — the entire wire format between a runner and the orchestrator.
  • Kept equivalent across all eight templates. Change the loop in one and you change it in all of them, and in PROTOCOL.md, or they stop being comparable.

The easiest way to measure nothing

The classic micro-benchmark trap: if nothing observes your operation's result, an optimizing compiler is free to delete the whole computation — and you measure nanoseconds of nothing, a confident wrong number. gota's templates guard against it in every language.

Compiled languages — dead-code elimination. In C, Rust, Go, and Zig a result that is never read can be optimized away under -O2. Each runner consumes its op's result through a one-byte sink, so the work has to survive.
Haskell — laziness. The sharpest case: in a lazy language an unforced computation is a thunk that never runs, and an invariant one is computed once and shared. The harness threads each result into the next call and forces it, so the work can be neither deferred nor shared. Same hazard, made a language requirement.
Why it matters here. Peak-of-batches is only honest if the batches did real work. The sink discipline is what keeps the number a measurement instead of a fiction — the same reason every runner.* carries a sink and Haskell's seam is shaped the way it is.

Honest by design

gota reports peak (unimpeded) throughput on a normal machine, which is right for comparing implementations and optimistic for real-world latency. It says so, and it never invents a number.

What the numbers mean. Peak-of-batches is the unimpeded rate, not a latency figure, and naive code shows language/runtime overhead, not how fast tuned or SIMD code can go. Every figure comes from a real run on a stated machine; RESULTS.md, results.json, and report.html are generated, never hand-edited, and CI numbers (whose hardware varies) are never committed.

It is also deliberately not some things: not a nanosecond-latency tool (use criterion or JMH for that), and not lab-grade absolute numbers (for those you would pin cores and lock frequency on a dedicated box). It is a comparable picture, drop by drop.

Copy and run

Copy harness.py and the templates/<lang> for each language you benchmark, swap your operation into each runner.*, write a small run.py, and run it. The bundled example ties four languages together end to end.

# run one language's runner standalone (prints one JSON line)
python3 templates/python/runner.py 65536 0.2 0.4

# run the full example: build + run every runner, then tabulate
python3 examples/run.py            # writes results.json + RESULTS.md
python3 report.py examples/results.json -o examples/report.html