Skip to content

defk-w8l: one countdown for the safepoint budget instead of two counters - #81

Merged
DanielLuning merged 4 commits into
mainfrom
defk-w8l/yield-bookkeeping
Oct 22, 2026
Merged

DanielLuning merged 4 commits into
mainfrom
defk-w8l/yield-bookkeeping

Conversation

@DanielLuning

@DanielLuning DanielLuning commented Oct 22, 2026 •

Copy link
Copy Markdown
Owner

What changed

Stacked on #80 (defk-w8r.3), which is stacked on #79: merge #79, then #80, then this.

The yield bookkeeping of a kernel kept two values live, the tick count and the budget. A safepoint now counts one value down from the budget and tests it against zero (DEFK_SP, DEFK_TICKS in sp_macro/3; the preamble in run_preamble/1); the budget lives only in the clock (defk_clk.budget), where defk_tick keeps it anyway. Same semantics: ticks >= budget is budget - ticks <= 0, and a resume restores the saved budget as before. The header's interface list loses defk_budget. No runtime header code changed.

A new bench, bench/keccak_yield.exs, compiles examples/keccak.exs' kernels twice (yielding, yield: false), counts the stack loads and stores in the hottest round block of the assembly (cc -S with the build's flags, cut at the loop's back-edge), and times one 16 MB message interleaved. The breakdowns are docs/performance.md 6.46; 6.4 and 6.8 point at it.

Results

  • fuzzy_search (bench/yield_cost.exs, 12 schedulers, 20 queries x 5 rounds): the per-word gap to the yield: false twin goes from +30-31% (three runs) to +16-17% (three runs), about 5 ns to 3 ns per word on 17.
  • Keccak 16 MB: unchanged, 12 stack stores and 12 loads per round and 1.30-1.34x the blocking kernel (quiet; 6.8's 9-11 and ~10% were under load). The entry shows why no counter change can help: with the safepoint's saves removed the round loop spills 4, with the resume dispatch also removed 1 (the yield: false shape); the counters are none of the 12. A redesign of the saves or the dispatch is out of this ticket's scope and is noted as such.
  • Two more variants measured and not adopted: charging a plain short loop on entry (nothing beyond the countdown), and no bookkeeping register at all (countdown and resume flag in the frame, env in the clock): Keccak unchanged, fuzzy_search +19.6%, the memory read-modify-write per word costs more than the register it frees.
  • The tick gap (bench/tick_gap.exs, 3 runs a side): the yielding kernel's p99 2.27 ms against 2.25 on main, worst 2.3-2.5 against 2.3; unchanged.

How it was checked

mix ci on the M-series laptop: 2309 passed, 817 excluded (forced-yield tests included); mix test once more before the ci run. test/runtime_interface_test.exs passes with the shortened interface list.

  • Touches a promised surface? The runtime contract's observable behaviour (tick gap, yields, resume) is unchanged, measured above; the generated C changes for every yielding kernel. No CHANGELOG.md entry.
  • Touches test/stability/? No.

Adoption rule

The ticket asked for a change to be adopted only when spills drop and time improves beyond noise on both Keccak and fuzzy_search. The countdown meets it on fuzzy_search (three runs each side) and explains Keccak without changing it. Adopting it anyway is this PR's proposal, as it simplifies the bookkeeping and loses nothing; owner's call.

…two arguments bounds the later one

A gather loop (bench/gather_layer.exs) lists the checks its hot loop
keeps. Most of the handwired study's list was already proven away; one
was not for a reason of ranking: facts land on a condition's newest
atom, and parameters ranked by their C names, so base <= length(x)
bounded the length and not base, and base + idx[j] kept its overflow
check. The facts scope now carries the parameters' declaration order,
and the ranking (newest/2, the product's canonical form) reads it; a
scope without one still ranks by name (the unit tests' scopes). The
gate shows one module changed, which lost a range-limit test; the
guarded gather loop runs about 0.75 ns per term where it was 0.9-1.1;
the compiler does +0.5% of its work. Recorded as docs/performance.md
6.45; the rule for users in docs/language.md (raising guards).
The yield bookkeeping of a kernel kept two values live, the tick count
and the budget; a safepoint now counts one value down from the budget
and tests it against zero, and the budget lives in the clock, where
defk_tick keeps it anyway. Same semantics (ticks >= budget is
budget - ticks <= 0; a resume restores the saved budget).

Measured (docs/performance.md 6.46): fuzzy_search's per-word gap to its
yield: false twin falls from +30% to +16%; Keccak's round loop is
unchanged (12 spills per round, 1.3x), and the entry shows why: 8 of its
12 spills are the safepoint's saves and 3 the resume dispatch, none the
counters. Two other variants (charge a plain short loop on entry; no
bookkeeping register at all) gained nothing and are recorded.
bench/keccak_yield.exs counts the spills and times 16 MB; the tick gap
and the test suite are unchanged.
DanielLuning pushed a commit that referenced this pull request Oct 22, 2026
…one countdown instead of two counters; fuzzy_search +30% -> +16%, Keccak unchanged (its 12 spills are the saves (8) and the resume dispatch (3), not the counters; 1.3x quiet). Variants V2 (charge on entry) and V3 (no bookkeeping register) measured, not adopted. Tick gap unchanged, mix ci green. docs/performance.md 6.46. Stays in progress until the merge."
@DanielLuning
DanielLuning merged commit 74215eb into main Oct 22, 2026
@DanielLuning
DanielLuning deleted the defk-w8l/yield-bookkeeping branch October 22, 2026 10:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant