← all benchmarks

The --threadsafe I/O lock stops paying a syscall per statement

feature-a-io-lock-owner-from-tls-not-gettid, landed 2026-08-31.

Box: this dev machine, load ~4 (Track T's watcher running). Program: bench/threadsafe_io_writeln.pas — 400,000 Writeln('x') to /dev/null. Method: interleaved A/B, seven rounds, min of N (means are meaningless against a loaded box).

build compiler sha256 min of 7
no --threadsafe 8301ce20d5b9 0.48s
--threadsafe, before 13ff03adbf6d 0.71s
--threadsafe, after 8301ce20d5b9 0.49s

43% overhead → nothing measurable. strace -c on the same binary, before and after:

syscall before after
write 800000 800000
gettid 400000 1
getrlimit 0 1

One gettid per I/O statement was a third of all syscalls. It is now one per process, cached in the thread's TLS block (TLS_SLOT_TID) and read behind the stack-bounds check that makes reading it safe.

What this does NOT measure

Writeln is still two write(2) calls, payload and newline, and that is now the entire cost of this workload — 800,000 syscalls against the 400,001 the lock used to add. Buffering console output would beat this whole change on this shape, and it would help the unlocked build too. Separate ticket; the ratio above is what remains after the lock is free.

What is still on the slow path, deliberately

Cloned threads. The clone stub knows the top of the child's stack and not the bottom — only whoever allocated the stack knows that — so their bounds stay zero, which is the fail-safe case: they miss the fast path and pay gettid, exactly as before this change. Foreign threads (glibc pthread_create) also miss, and that is not a limitation but the entire point: they read a block they inherited and must not be believed.

So the measured win is the main thread's, which is where a single-threaded --threadsafe program does all of its I/O.