Skip to content

Exp 268: leave empty statement caches virtual - #303

Merged
danReynolds merged 2 commits into
mainfrom
exp-268-lazy-stmt-cache-init
Aug 12, 2026
Merged

Exp 268: leave empty statement caches virtual#303
danReynolds merged 2 commits into
mainfrom
exp-268-lazy-stmt-cache-init

Conversation

@danReynolds

@danReynolds danReynolds commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Hypothesis

Exp 267's 128-entry statement caches eagerly dirty unused pages during open: stmt_cache_init clears the complete array even though the parent database allocation is already zeroed, consumers inspect only [0, count), and insertion fully initializes each slot before publishing it. Making initialization count-only should recover roughly one 273,408-byte cache per reader without changing cache behavior.

Approach

  • Remove the redundant 128-entry clear from stmt_cache_init; retain count = 0.
  • Add a native-asset-aware AOT open/RSS harness with isolated 2-reader and 4-reader lanes.
  • Verify the emitted arm64 code, then collect 12 alternating-order RSS passes and 6 alternating-order open-time passes against origin/main at 96e6730.
  • Guard first-slot use, exact 128-entry fill, perpetual eviction, dependency reliability, allocation-failure fallback, and 400-statement correctness.
  • Publish the full receipt, experiment/index/signal sources, a transferable JOURNAL lesson, and correct the current signal-map reference from the old 32-entry row-memory cap to 128.

Results

lane baseline candidate delta
2-reader max RSS 18,923,520 B 18,333,696 B −589,824 B (−3.12%)
4-reader max RSS 21,012,480 B 19,955,712 B −1,056,768 B (−5.03%)
2-reader native open 602.0 µs 587.4 µs −2.42%
4-reader native open 757.6 µs 736.9 µs −2.73%

The candidate was lower in all 48 paired RSS observations. Optimized baseline code contains one _bzero(..., 0x42c00) per reader; candidate code omits it. Pressure controls were neutral by median: point1 −0.48%, rotate128 −0.61%, and churn-unique −0.51%.

Outcome

Accepted. This recovers most of exp267's +0.8–1.2 MB pool RSS price with a deletion-sized runtime change and no new allocation, OOM, fragmentation, or ownership path. The actual cache entry is 2,136 bytes, correcting exp267's approximate 1.6 KB figure. Exact-sized dependency metadata remains open only if a representative populated-cache trace justifies it.

This PR changes native runtime code, so the automation will not merge it; it should remain open for human approval after CI and automated review are green.

Test plan

  • dart run benchmark/finalize_experiment.dart --experiment=experiments/268-stmt-cache-lazy-init.md
  • dart run benchmark/check_experiment_dispositions.dart
  • dart run benchmark/check_knowledge_links.dart
  • dart run benchmark/generate_knowledge_page.dart --check
  • dart analyze --fatal-infos
  • dart test -j 1 (394 tests)
  • Focused 23-test cache/dependency/fault suite
  • AOT pressure guards: point1, rotate128, churn-unique
  • git diff --check

@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Belief impact

Learned

  • 268.1 · Leave empty statement caches virtual
    Making stmt_cache_init count-only reduces median max RSS by 589,824 bytes at two readers (18,923,520 -> 18,333,696, -3.12%) and 1,056,768 bytes at …
  • 268.2 · Leave empty statement caches virtual
    The actual resqlite_cached_stmt layout is 2,136 bytes and one 128-entry cache is 273,408 bytes (0x42c00). Baseline arm64 AOT code calls _bzero
  • 268.3 · Leave empty statement caches virtual
    Count-only cache initialization does not regress native open, fill, lookup or eviction. Six alternating-order AOT passes improve median-of-pass nativ…

What this changed

Exp 267's measured +0.8-1.2 MB statement-cache RSS was real, but most of the four-reader price was not retained prepared statements or populated dependency metadata. resqlite_db comes from calloc, then stmt_cache_init wrote the complete 128-entry array again; optimized arm64 removed that redundant write for the adjacent writer cache but retained a 273,408-byte bzero for every reader. Four readers therefore eagerly touched 1,093,632 bytes before any cache slot became live. Removing that clear recovers 1,056,768 bytes of median max RSS, almost the whole observation that named dynamic metadata as the next target.

The native statement-cache entry is 2,136 bytes, not the approximate 1.6 KB carried from exp 248 into exp 267. resqlite_column_dep is 24 bytes, so dep_columns[64] alone occupies 1,536 bytes; one 128-entry cache is exactly 273,408 bytes (0x42c00). The corrected size strengthens exp 267's eviction-compaction explanation but changes the footprint follow-up: unused capacity can stay virtual without changing entry ownership at all.

A count-bounded inline array has an explicit publication boundary. stmt_cache_lookup_entry and stmt_cache_clear inspect only [0, count), while stmt_cache_insert fully zeroes the selected slot through stmt_cache_entry_init before incrementing count. Under that invariant, clearing capacity at container construction is not defensive initialization; it is an eager page-fault policy. The 400-statement eviction suite and dependency-reliability tests remain green because every live and recycled slot still takes the same full initialization path.

A source-level calloc argument is insufficient evidence that unused capacity stays non-resident. Dead-store elimination differed by placement inside one function: the compiler removed the writer-cache clear adjacent to calloc and retained the identical reader-cache clear inside the connection-open loop. The durable attribution paired optimized assembly with two- and four-reader fresh-process RSS; source inspection alone would have predicted either all clears or no clears and missed the actual binary.

Dynamic dependency metadata is no longer the first-order response to exp 267's memory number. It may still shrink a populated 128-entry cache and make a future capacity raise cheaper, but it adds one or two allocations per prepare, failure-conservative reliability semantics, fragmentation and disposal ownership. After empty capacity stays virtual, that complexity needs a representative filled-cache or >128-statement trace; empty-open RSS cannot justify it.

@danReynolds danReynolds added codex codex-automation type: performance Implementation experiment changing a runtime hot path approved Experiment succeeded: a kept win or a passing guard labels Aug 11, 2026
…he-init

# Conflicts:
#	experiments/JOURNAL.md
@danReynolds
danReynolds merged commit fb7213c into main Aug 12, 2026
7 checks passed
@danReynolds
danReynolds deleted the exp-268-lazy-stmt-cache-init branch August 13, 2026 10:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Experiment succeeded: a kept win or a passing guard codex codex-automation type: performance Implementation experiment changing a runtime hot path

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant