Skip to content

A12: every battery without exceptions or RTTI, and the hot path made the engine's own - #97

Open
tap wants to merge 2 commits into
mainfrom
claude/sample-rate-solutions-comparison-mqc190
Open

tap wants to merge 2 commits into
mainfrom
claude/sample-rate-solutions-comparison-mqc190

Conversation

@tap

@tap tap commented Nov 5, 2026

Copy link
Copy Markdown
Owner

What this changes

Two commits, one item: the roadmap's A12, from Q5's answer that a current project builds all three engines with -fno-exceptions -fno-rtti on the M33, M55 and Hexagon.

Commit 1 — every battery runs in that configuration.

  • TAP_SR_NO_EXCEPTIONS_RTTI (root, default OFF) builds every target with -fno-exceptions -fno-rtti. It is an option rather than CMAKE_CXX_FLAGS, which would replace the cross toolchains' flags.
  • tests/support/tap_sr_test/expect_rejected.h: TAP_SR_EXPECT_REJECTED, TAP_SR_EXPECT_REJECTED_WITH and TAP_SR_EXPECT_ACCEPTED. With exceptions they are EXPECT_THROW, a throw whose what() is validate()'s text, and EXPECT_NO_THROW; without, a death test on std::terminate() where GoogleTest has death tests, and the statement run. The six test files that used EXPECT_THROW or a try of their own go through them, test names unchanged.
  • CI: the hosted no-exceptions job now runs every battery, the examples and the C ABIs; the nine QEMU legs run again in that configuration.
  • ROADMAP: Q5 confirmed, A12 and P14 added, the verdict rule and C3's freeze updated; PLAN.md D17; README; a CLAUDE.md rule (no EXPECT_THROW or own try in a test).

Commit 2 — the hot path becomes the engine's own. The measurement found the ratchet's counts were not the engines'. Built the new way with the engines inlined into the workloads' main as before, rational's M55 float interpolators read +39% and +53% and bridge's M55 Q31 up row +15%, with every checksum unchanged.

  • The cause, from the disassembly: DspTap's dot kernel compiles to the same instructions either way (diffed alone, both ways: only unwind-table directives differ). Inlined into a 2 KB main whose control flow had lost its EH landing pads, GCC's register allocator left the inner MAC loop's two pointers on the stack. The loop went from 6 instructions a tap to 10; the four extra were a load and a store of each pointer. The baselines had been measuring the caller's register allocation.
  • bridge: process, pull and flush out of line (TAP_SR_BRIDGE_NOINLINE). Within ±0.7% of every baseline on all three targets, both configurations identical (PLAN.md M7f).
  • rational: the three entries out of line (TAP_SR_RATIONAL_NOINLINE); each block runs against a cursor (the stream state and the hot constants as locals, open()/close()); the history is one planar allocation; the channel count is dispatched to a compile-time constant for one and two channels (process_block<Ch>, as bridge's process() does for its taps — the lever rational's PLAN.md section 6 had already named). The cursor exists because an out-of-line this is reachable from compact()'s memmove, so any member read in the loop was a reload per frame.
  • The ratchet jobs (QEMU and host) now build the exceptions-off configuration too and count it against the same baselines, so the identity of the two configurations is gated, not asserted.
  • rational's baselines re-recorded on every target and its ceilings on the three, with the reason in PLAN.md section 6. The footprint formula lost its P ch sizeof(std::vector) term; the README table is regenerated from the test.

Why

A3 made the engines build without exceptions; no test had run that way, nothing had been built with -fno-rtti, and once measured, the configuration the consumer ships ran up to 53% slower for a reason outside the engines. The 1.0 freeze now covers the configuration (P14), with every battery and the ratchet gating it.

Verification

Batteries, both configurations, clang 18, -Werror, C ABIs on: every engine 343/343 before the lever; after it, rational 138/138 and bridge 110/110 with and without exceptions. GCC 13 passes the same with exceptions on. Bit-identity: the scipy vectors, the 182-row matrix, the DspTap cross-check and every ratchet checksum unchanged.

QEMU legs, exceptions off: all nine passed locally (M33: rational 9, bridge 8, async 8; M55 the same; Hexagon: rational 121, bridge 105, async 64) before the lever, then rational and bridge again after it.

Ratchet, final shape, both configurations, against the previous baselines (M33 and M55: arm-none-eabi-gcc 13.2.1, QEMU 8.2.2; Hexagon: clang 19.1.5; host: g++-13 under callgrind, as CI runs them):

Target rational (18 rows) bridge async
M55 configs identical on all rows; 17 of 18 improved: Q15 −9.5…−17.8%, chain −12.9%, float interpolators within +0.65%, ↓3 float −8.4%; one row up, ↓2 float +1.8% within ±0.7%, identical unchanged
M33 identical to within 0.26%; worst +2.0%, improvements to −16% within ±0.23% unchanged
Hexagon identical; worst +0.22%, improvements to −9.2% +0.000% (clang had never inlined the entries) unchanged
host identical; stereo rows −4.8% and −5.0% (re-recorded), 12-channel within +0.2% within ±0.23% +0.000%

Ceilings (rational, re-recorded): engine .text +25% (M33/M55) to +50% (Hexagon Q15) for the three channel instantiations; image +2…+5%; M33 per-call stack 184–208 B against 88–128 B before, the out-of-line frame. bridge's and async's did not move.

Declined on measurement (M55, both configurations): emit() out of line per output (identical, floats +4.8%); the cursor with the entries inlined (floats −1.5…−2.8% but the chain +13.4% without exceptions and the configurations apart again); -fno-version-loops-for-strides on the outlined entries (−1.5 points only).

Style: clang-tidy clean on every changed TU in both configurations; clang-format; check_retired_words; the icount and ceilings README tables pass their freshness checks.

Not run here: Windows (unchanged, exceptions on) and the arm64 TSan workflow; CI is their gate.

Notes for the reviewer

  • Submodule pin unchanged (DspTap v0.1.0). The substrate half of A12 is The battery without exceptions or RTTI: TAP_DSP_NO_EXCEPTIONS_RTTI DspTap#63, green and open; nothing here needs it.
  • Contract: no header's numeric contract moves; outputs are bit-identical. basic_stage's history layout is private. emit is now static in rational's stage (the book's rational_emit include updated).
  • Behaviour change for consumers: the per-block entries of bridge and rational are no longer inlinable. A consumer's own inlining of them was never a promise, and it is what made their cost depend on the caller.

Generated by Claude Code

Q5's answer: a current project builds all three engines with
-fno-exceptions -fno-rtti on the M33, M55 and Hexagon. A3 made the
engines and C ABIs build that way; no test had run there, and nothing
had been built with -fno-rtti.

- TAP_SR_NO_EXCEPTIONS_RTTI (root, OFF): every target compiles C++ with
  -fno-exceptions -fno-rtti (an option: CMAKE_CXX_FLAGS would replace
  the cross toolchains' _INIT flags). GCC and Clang only.
- tests/support/tap_sr_test/expect_rejected.h: TAP_SR_EXPECT_REJECTED,
  _REJECTED_WITH and _ACCEPTED. With exceptions they are EXPECT_THROW,
  a throw whose what() is validate()'s text, and EXPECT_NO_THROW;
  without, a death test on std::terminate() where gtest has them, and
  the statement run. The six test files that used EXPECT_THROW or a
  try of their own go through them; their names are unchanged.
- CI: the hosted no-exceptions job (name kept) now runs every battery,
  the examples and the C ABIs; the nine QEMU legs run again with the
  option, each with its own exclusions and budget.
- ROADMAP: Q5 confirmed, A12, P14, the verdict rule and C3's freeze;
  PLAN.md D17; README (error model, options); CLAUDE.md's rule.

Host: GCC 13 and Clang 18, Release, -Werror, C ABIs and examples on:
343/343 with exceptions and 343/343 without, both compilers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GKzq1VSdcMYCRMhddiMaf
The no-exceptions measurement found the ratchet's counts were not the
engines': built with -fno-exceptions -fno-rtti and the engines inlined
into the workloads' main as before, rational's M55 float interpolators
read +39 ... +53 % and bridge's M55 Q31 up row +15 %, every checksum
unchanged. The disassembly: DspTap's dot kernel compiles to the same
instructions either way; inlined into a 2 KB main whose control flow had
lost its EH landing pads, GCC left the inner MAC loop's two pointers on
the stack, 6 instructions a tap becoming 10. The engine had not changed;
its caller had, and the baselines had been measuring the caller's
register allocation.

So the hot path becomes the engine's own:
- bridge: process, pull and flush out of line (TAP_SR_BRIDGE_NOINLINE).
  Measured on the M33, M55 and Hexagon in both configurations: within
  +-0.7 % of every baseline, the configurations identical (PLAN.md M7f).
- rational: the entries out of line (TAP_SR_RATIONAL_NOINLINE); each
  block runs against a cursor (the stream state and the hot constants as
  locals, open()/close(): an out-of-line this is reachable from
  compact()'s memmove, so a member read in the loop was a reload per
  frame); the history one planar allocation; the channel count dispatched
  to a compile-time constant for one and two channels (process_block<Ch>,
  as bridge's process() does for its taps: the lever PLAN.md 6 had named).
  Measured in both configurations on every target, against the previous
  baselines: identical to the instruction with and without exceptions on
  every row; M55 17 of 18 rows improved (Q15 -9.5 ... -17.8 %, the chain
  -12.9 %, up2_float +0.5 %, down2_float +1.8 % the one row up), M33 to
  -16 % (worst +2.0 %), Hexagon to -9 % (worst +0.2 %), host stereo -5 %.
  Three variants measured and declined, in PLAN.md section 6.
- The ratchet jobs (QEMU and host) build the exceptions-off configuration
  too and count it against the same baselines: the identity is gated.
- rational's baselines re-recorded on every target; its ceilings on the
  three: engine .text +25 ... +50 % (three channel instantiations), the
  M33 call stacks +64 ... +104 B (the out-of-line frame), the reason in
  PLAN.md 6. Footprint: the history's P ch sizeof(std::vector) term is
  gone (test_footprint.cpp, the README's table regenerated from the test).
- Batteries: rational 138/138 and bridge 110/110 under clang in both
  configurations; bit-identity by the scipy vectors, the matrix and the
  ratchet's checksums. clang-tidy clean both ways.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GKzq1VSdcMYCRMhddiMaf

This branch has not been deployed

No deployments
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.

2 participants