9. Performance#
Measured facts from Setu's notes (ns per call, tool/bench.py, A/B interleaved, best of several runs, on all four interpreters):
- Most per-call cost was store-forwarding stalls: a value written in small parts (CPython filling a
Py_buffer, an inlinedError!T, an optional's tag) then read with one wider load. So arguments are filled in place in the arguments tuple (BufferArg.fill), big argument structs are not copied, and slow paths stay out of line (intArg,outOfRange,notWritable:@branchHint(.cold)). - Whether a copy happens depends on LLVM's inlining, so identical source can run 2x apart between builds: compare A/B builds with
objdump -d, interleaved, best of several runs. inlinechanges nothing infastbuilds (-Doptimize=fast): LLVM inlines the call path anyway. A moved trampoline can read ~1 ns slower with identical code (placement).tool/bench.py --so a --so bcan read the second build 1-1.5 ns slower on the cheapest call (bench_nop): swap the order before believing such a gap.- A flag on an argument (marking a
taken copy) cost 6-8 ns per buffer argument;taketakes a second view instead.OwnedBuffersaves that view (24-43 ns in Liburing's buffer preps). - Reference points: Shakti's
loop_benchnop serial ~204 ns, batch ~129 ns,SQE()~48 ns; Liburing'sprep_recv(sqe, -1, bytearray)~22 ns.
When touching src/class.zig / src/export.zig / src/convert.zig / src/arg.zig: python3 tool/bench.py --ref HEAD before claiming a change is neutral, and zig build profile (section 11) before and after: instructions per call are exact on every run, so a change of one instruction shows, and --lines says which line it is on. Time and instructions together tell a change that saves work from one that only moves it (a stall costs time, not instructions).