Experiment 215: Persistent executeWrite result-buffer slot (revisits exp 095)

Date: 2026-07-04

Status: Rejected

Direction:parameter-encoding-and-binding, measurement-system

Benchmark Run: focused

benchmark/experiments/write_result_direct_read.dart;

raw pair tables in

benchmark/results/2026-07-04T11-13-17Z-exp215-persistent-write-result-slot.md.

Archive:archive/exp-215

Problem

executeWrite() in

lib/src/native/resqlite_bindings.dart

allocates a 16-byte native scratch buffer on every writer request so C can

return affected_rows and last_insert_id:

 final resultBuf = calloc<ffi.Uint8>(_writeResultSize); try { final rc = resqliteExecute(..., resultBuf); ... final view = ByteData.sublistView( resultBuf.asTypedList(_writeResultSize), ); return WriteResult( view.getInt32(_writeResultOffAffected, Endian.little), view.getInt64(_writeResultOffLastId, Endian.little), ); } finally { calloc.free(resultBuf); } 

Exp 095 already tried removing this per-call

calloc / free pair with a persistent per-isolate buffer and rejected the

change: the release-suite comparison reported "0 wins, 14 regressions, 139

neutral" and focused dispatch shifted only a couple of microseconds. That

verdict was correct given the release-suite harness resolution — write-path

medians are in the milliseconds and cannot see a few-hundred-nanosecond

scratch.

Two things have moved since exp 095:

benchmark/experiments/write_result_direct_read.dart, a focused writer

harness that reports median microseconds per Database.execute() call over

2000 calls/sample on three shapes (no-op update, point update, param

update). That is exactly the µs-precision writer harness

exp 211's Future Notes

named as the revisit condition for exp 095.

change on the reader side: persistent per-isolate slots for queryBytes()

reproduced −3.4 % to −6.6 % on tiny-rowset lanes across two order-flipped

passes of select_bytes_repeated_calls.dart after exp 108 had rejected the

same mechanism under the ms-precision release suite.

The named revisit condition from exp 095 is present.

Hypothesis

Reusing one persistent 16-byte per-isolate slot removes one calloc + one

matching calloc.free (~a few hundred nanoseconds each) per

Database.execute() call. On write_result_direct_read.dart's ~6.5 µs

no-op-update floor the mechanism should land in a low-single-digit

candidate-faster band; on the ~14 µs point/param lanes the same fixed

mechanism should be about half as big.

Prediction:

order-flipped passes.

the point/param lanes show smaller-but-same-sign wins because the

mechanism is a per-call fixed cost.

Reject if the deltas do not reproduce same-sign across the order-flipped

pair, or if the mechanism sits at or below the harness noise floor.

Approach

Single-file change to

lib/src/native/resqlite_bindings.dart.

Add one top-level final slot pointer near executeWrite():

 final ffi.Pointer<ffi.Uint8> _writeResultBuf = calloc<ffi.Uint8>(_writeResultSize); 

Replace the per-call calloc<ffi.Uint8>(_writeResultSize) inside

executeWrite() with _writeResultBuf and remove the matching

calloc.free(resultBuf) in the finally clause. The ByteData decode stays

byte-for-byte identical (exp 214 already rejected changing that half of the

path). This mirrors exp 211's pattern: queryBytes uses _queryBytesOutBuf

/ _queryBytesOutLen / _queryBytesOutRowCount at file scope, shared

across all reader-worker selectBytes() calls on the same isolate.

Correctness rests on three invariants:

runs to completion before the next request is delivered

(write_worker.dart). executeWrite

reads both scalar fields inside the same synchronous FFI-return frame

before the handler yields, so a second executeWrite call cannot race the

slot.

both affected_rows and last_insert_id before returning; on rc != 0

the Dart caller throws before reading the slot, so a previous successful

call's values cannot leak into an error frame. The _handleMultiExecute

loop reads the WriteResult into an outcomes list before the next

executeWrite call runs, so back-to-back invocations are equally safe.

read, so isolates that never call executeWrite never allocate it. The

standing cost is 16 bytes per writer isolate.

Results

Raw pair tables (four A/B passes, medians and mins/maxes) are recorded in

benchmark/results/2026-07-04T11-13-17Z-exp215-persistent-write-result-slot.md.

Candidate vs matching-pair baseline, negative = candidate faster:

ShapeP1 (b -> c)P2 (c -> b)P3* (b -> c drifted)P4 (b -> c rerun)
noop update+1.5 %-0.9 %-25.4 %*-4.2 %
point update-0.5 %-2.2 %-12.1 %*-4.1 %
param update-0.1 %-3.3 %-5.3 %*-5.2 %

*Pair 3's baseline pass shifted well above the other three baseline

passes (noop median 9.115 vs 6.5-6.8 elsewhere), so pair 3 is a

system-drift outlier and does not enter the verdict.*

The candidate us/call medians are stable across all four pairs (noop

6.54-6.80, point 13.94-14.30, param 13.99-14.19); it is the baseline that

jitters. Pairs 2 and 4 both show all three shapes candidate-faster in a

coherent 0.9-5.2 % band — the pattern a real ~200 ns per-call mechanism

should produce. Pair 1 does not reproduce that pattern: the no-op-update

lane goes the wrong direction (+1.5 % candidate-slower), which is exactly

the shape a fixed-cost mechanism should show its largest relative win

on. When the lane most sensitive to the mechanism cannot reproduce

same-sign across the primary order-flipped pair, the mechanism has not

cleared the harness floor.

Decision

Rejected. Do not keep the persistent 16-byte writer-result buffer.

The mechanism removes one native heap calloc / free pair per

Database.execute() call — structurally sound and analogous to exp 211 —

but the deltas do not reproduce same-sign across the primary order-flipped

pair (P1 + P2). Cleaner-condition passes (P2, P4) each show a coherent

candidate-faster shape, but P1 flips the sign on the lowest-baseline lane;

that inconsistency is the exp 214 pattern on this same harness.

Together with exp 214 — which rejected the mirror change on the other

half of the path (direct pointer read instead of the ByteData decode) — this

closes both Dart-side writer-result micro-optimizations on

write_result_direct_read.dart. The 6.5-14 µs writer floor is not

calloc-limited or view-limited on the shapes this harness measures; the

remaining wall is SQLite step, isolate transport, and Dart-side param

handling.

Would reopen if: (a) a lower-noise writer harness surfaces where

the ~200 ns per-call mechanism cleanly clears the floor, or (b) a workload

appears where writer-result decode is a larger share of wall time

(long-lived hot single-writer paths where the exp 159 pipelining has already

compressed everything else). Exp 095's release-suite verdict, exp 214's

decode rejection, and exp 215's scratch rejection now form a consistent

writer-result path is not the next lever signal — future runners should

look up-stream at param encoding or isolate dispatch rather than at another

executeWrite micro-change.

Test plan

benchmark/experiments/write_result_direct_read.dart (four passes; see

Results)