srdif: tune for Hexagon (clang) and gate Hexagon in the ratchet; no output bit moves - #44
Conversation
The srdif engine (#42) met "must not regress" on every key this ratchet measured, but none of them is Hexagon, the third target of MuTap, DspTap's main consumer: MuTap's Hexagon ratchet read +12 ... +13 % on its chain workloads at the bump to 72977aa. This adds the target so DspTap gates it directly: - cmake/hexagon-linux-musl.cmake, adapted from MuTap's (the same CodeLinaro clang 19.1.5 toolchain, -mv68 -mhvx -mhvx-length=128b, static musl), so the two repositories' Hexagon counts are comparable. - scripts/icount.py: target `hexagon`, run under qemu-hexagon user mode. - bench.yml: a `hexagon` key in the matrix, which builds a plugin-enabled qemu-hexagon from the pinned QEMU 8.2.2 release (cached on its digest), downloads the toolchain, and measures the size probes with llvm-size. As a hosted target it also builds and gates rfft_f64_512. The key has no baselines yet, so its job refuses on a pull request until a workflow_dispatch run on this branch seeds them (bench/README.md, "Seeding"); the .text ceilings are 0 (not recorded) until then. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy
…he Arm keys The srdif engine (#42) executed more instructions on the new `hexagon` ratchet key (clang 19.1.5, -mv68 -mhvx, qemu-hexagon 8.2.2) than the engine it replaced: +15.9 % / +19.8 % (float, N = 512 / 2048) and +7.2 % (double). Now -7.64 % / -5.88 % and -3.20 % below it, and every Cortex-M float scenario falls as well (-0.06 ... -1.11 %). - Under clang (not at -Os / -Oz), the butterflies, groups, compile-time blocks, their index-sequence lambdas, the post-pass pairs and the swaps are forced inline (TAP_DSP_SRDIF_INLINE). clang had kept them out of line on Hexagon: calls per group and register leaves through a stack array. GCC keeps its own choices (forced there, the FPU keys cost 4.5 - 9.1 % more). - The post-pass runs two bin pairs with every load before any store (post_two), so the pairs' arithmetic can share Hexagon packets. - Blocks of 16 and fewer run one split-radix level at a time in memory down to pairs (level, level_bf) instead of register leaves: a leaf of 16 holds 32 values and spills on every target measured. - Double runs the run-time fused pass one group per loop step (two groups of doubles are twice Hexagon's register file). - mul_sub spells a b - c d so clang's statement-scoped contraction fuses a multiply-subtract instead of negating a product (float only). No output bit moves at -ffp-contract=off: the srdif fingerprints pass unchanged on the host (g++, clang++), the four QEMU legs and Hexagon. Contracting builds move last bits (bench checksums on the FPU keys and Hexagon float). Contract, tables and heap unchanged. The measurements and what was tried are in docs/fft-design.md, "Hexagon (clang) tuning". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy
… tuning The srdif engine's Hexagon tuning lowers every float scenario on the four keys that run it, inside the +-3 % band (m4-softfp -0.06 / -0.06 %, m4f -1.11 / -1.06 %, m33 -0.38 / -0.49 %, m55-ooura -0.36 / -0.51 %, N = 512 / 2048); the exact counts are recorded, one row per scenario in bench/README.md. Fixed-point scenarios and the m55 (CMSIS) key are unchanged to the instruction and not re-recorded. The m4f DONE line in the README carries the new checksum (GCC fuses the unchanged operations differently; the fingerprints at -ffp-contract=off do not move). The hexagon key is seeded from CI, not here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy
A new subsection of "The floating engine (srdif)": what the hexagon key counts (qemu-hexagon counts packets, not instructions), where the count went at 72977aa, the before/after per key and scenario, each step and each change measured alone, what was tried and not kept, the output bits (the fingerprints hold at -ffp-contract=off, Hexagon included; which contracting builds' checksums move and why), the .text probes, the Hexagon test battery, host timings, and a finding for the class: on Hexagon a third of every floating scenario is basic_real_fft's out-of-place copy through musl's memcpy. The "Structure" bullet on register leaves notes what the tuning replaced; README's CMSIS ratio (1.78x -> 1.77x at N = 2048) follows the new m55-ooura count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy
# Conflicts: # bench/README.md
- bench/README.md: the `hexagon` row of the key table, and why the key exists. It counts packets, not instructions: qemu-hexagon reports one guest instruction per VLIW packet. Its absolute counts carry a per-machine constant. Six required `icount <key>` jobs once seeded. - docs/fft-design.md, provenance: "The Hexagon tuning pass, clean-room". What the orchestrator measured of the port, and how that was passed on (numbers only). The brief's exclusions. The implementer's access statement, including the single lines of forbidden sections its broad greps printed (routine names and one-line prose, no code, reported unused). The result. - CLAUDE.md: the engine no longer has register leaves. Blocks of 16 and fewer run one level at a time in memory, with forced inlining under clang. It also names the hexagon key beside the five Cortex-M ones. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy
…ed engine Baselines: the counts of workflow_dispatch run 36363710480 on this branch (2a5fc69, seed mode), the tuned srdif engine. - rfft_f32_512: 45,934,423 - rfft_f32_2048: 51,608,903 - rfft_f64_512: 99,939,789 - Q15 / Q31: 159,976,886 / 184,890,581 / 157,303,237 For the record, the engine as #42 shipped it read 57,659,223 / 65,684,295 / 110,683,597 on the first seeding run (36357132506). The engine it replaced read 49,732,931 / 54,834,714 / 103,239,537 (local, the same harness, converted to the runner's constant). .text ceilings for the key: measured + 3 %, rounded up to 64 bytes, from llvm-size -A on the statically linked probes (musl and libc++ included): 245,824 / 251,968 / 251,456. The eight Cortex-M float rows re-recorded by the tuning pass are confirmed to the instruction by the same run (compare mode, +0.00 %). Their main SHA cells, like the hexagon rows', stay pending until the squash. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy
tap
left a comment
There was a problem hiding this comment.
Hostile review of #44 at 2bd44bb (srdif Hexagon tuning + hexagon ratchet key)
Verdict: no correctness defect in srdif.h. No output bit moves where the fingerprints are defined. Every headline number I re-measured reproduces, and I found no provenance problem. The findings below are one supply-chain gap in bench.yml, gaps in the clean-room record, and wording in the docs. This is a COMMENT review: I neither approve nor request changes.
What I ran, and what held
All of it was run on the PR head in a detached worktree, with builds in my scratch space.
- Output bits (
tap_dsp_srdif_fingerprint,-ffp-contract=off): the single pinned row matches in every configuration below.- Host g++ 13 Release: 437/437 tests pass.
- Host clang++ 18 Release (
TAP_DSP_WERROR=ON): 437/437 tests pass. cortex-m33andcortex-m4-softfpQEMU legs, built exactly asci.ymlbuilds them (MinSizeRel,-DTAP_DSP_WERROR=ON): 367 + 2 + 3 selected tests pass, and both fingerprints match.- Hexagon (clang 19.1.5, Release, so the forced-inline path is active) under
qemu-hexagonat every N from 4 to 65536: both profiles match.
- Hexagon battery. I built the whole Hexagon suite with the Hexagon toolchain file and ran it through ctest under
qemu-hexagon, with sweeps capped at 4096. 423 of 426 pass. The three failures arefft_abi_tag.*, whosedlopenreports "Dynamic loading not supported" in a static musl image. This matches the PR's report. - UB and memory, over the 241 FFT / srdif / fingerprint tests:
- g++ 13 with ASan+UBSan (
-fno-sanitize-recover=all): 241/241 pass. - clang++ 18 with
-fsanitize=undefined,bounds -fsanitize-trap=all, Release, so thealways_inlinepath is the one checked: 241/241 pass. - clang's ASan runtime is not installed in this container, so I could not run ASan under clang.
- g++ 13 with ASan+UBSan (
post_twoaliasing. The loop only reaches pairs k, k+1 ≤ m/2 − 1. Their four indices {k, k+1, m−k−1, m−k} are therefore pairwise distinct: m−k−1 ≥ m/2+1 > k+1. Loading all four before storing any cannot observe a store, so the reorder is exact.level<L>/block<L>. Checked by hand against the removedleaf_blocks<L>:- the same butterflies with the same twiddles, including J == L/8 as the eighth turn;
- J = 1 and J = 3 at L = 16 use W16^1/W16^3 and W16^3/−W16^1;
- the same sub-block recursion (L/2 at a, then L/4 at a+L and at a+3L/2 Samples);
block<1>is never instantiated.
- Portability of
TAP_DSP_SRDIF_INLINE, including the attribute on the templated lambdas:- clang 18 (host) and clang 19 (Hexagon) compile it at -O0, -O2 and -Os with
-Wall -Wextra -Wpedantic -Wconversion -Wshadowand no warning; - clang-cl (clang 18 in
--driver-mode=cl,/W4) compiles it; - clang-cl defines
__OPTIMIZE_SIZE__under/O1and/Osand not under/O2or/Od, so the size gate carries over to Windows. AppleClang uses the same front end; I could not run it here. - GCC never sees the attribute, so no compiler is left that would reject it on a lambda.
noexceptand allocation-free behaviour are unchanged: the new code is only stack aggregates and by-reference lambdas.
- clang 18 (host) and clang 19 (Hexagon) compile it at -O0, -O2 and -Os with
- Symbols. On the Hexagon Release binary,
llvm-nmshows that onlykernel,fused_pass,permute,post_passand the table builders survive out of line. Without the attribute,group<…>,block<16|32|64>,level<16>'s lambda andgroup_firstare all separate functions. - Hexagon counts, via
icount.py --target hexagonasbench.ymlruns it:- My machine reads a constant +1,596 against CI on all six scenarios, fixed point included. The PR head reproduces the seeded baselines exactly after removing that offset.
28befbd(#42's engine) reproduces run 36357132506's counts exactly after the same offset: 57,659,223 / 65,684,295 / 110,683,597.- PR run 36364049440 (
pull_request) reads the same counts as dispatch run 36363710480, to the packet.
- Arm keys,
m33andm4fbuilt exactly asbench.ymlbuilds them: the float counts equal the new baselines exactly (92,624,908 / 106,248,986 and 91,545,465 / 105,151,232). - Ablations, each change undone on the head tree:
| change undone | measured | claimed |
|---|---|---|
| forcing off (Hexagon f32 512 / f32 2048 / f64 512) | 54.13 / 62.12 / 107.12 M, +17.8 / +20.4 / +7.2 % | same |
forcing under GCC (m4f) |
+9.06 / +6.80 % | +9.1 / +6.8 % |
post_two (Hexagon, three scenarios) |
47.47 / 53.17 / 100.97 M | same |
mul_sub (Hexagon float) |
+0.65 / +0.43 % | same |
forcing at MinSizeRel (Hexagon f32 probe .text) |
263,204 B | same |
- The three Hexagon
.textfigures (238,628 / 244,580 / 244,132) and the three ceilings, each measured + 3 % and rounded up to 64 B, check out. - Packets, not instructions. I wrote a second plugin that also sums
qemu_plugin_insn_size()per executed unit. Onrfft_f32_512:- units: 45,935,997 (the ratchet's count);
- bytes: 382,797,700, which is 95,699,425 32-bit words;
- unit sizes: 1 word 18.5 M, 2 words 12.1 M, 3 words 8.5 M, 4 words 6.9 M.
- A plugin "instruction" is therefore a whole packet of up to four words. The
hexagonkey counts packets, and a word count would be about 2.08× higher. The claim holds.
- Provenance (item 4 of my brief; the port was read at
7a58ebefor this item only): no move toward the port. The side-by-side comparison is at the end.
Findings, most severe first
1. [medium] bench.yml:197-205: the Hexagon toolchain is downloaded without a digest check.
- The workflow checks the SHA256 of the plugin header and of the QEMU source. It does not check the one input that determines every
hexagoncount, the CodeLinaro compiler tarball. - Nothing asserts the compiler version either: "Toolchain versions" only prints it.
- Artifactory can replace
19.1.5/clang+llvm-19.1.5-cross-hexagon-unknown-linux-musl.tar.zstin place. If it did, the ratchet would move or silently re-baseline, and nothing would say why.bench/README.mdpromises the counts are "comparable with MuTap's Hexagon ratchet: same toolchain", which depends on this input being fixed. - Failure modes I checked:
- A failed download is loud:
curl -f→ zstd "unexpected end of file" → tar exits 2, reproduced locally underbash -e. - A wrong archive layout is loud:
test -n "$cxx"fails. - A missing
llvm-sizeis loud:pipefail, thencommand not found. - An empty
.textis loud: thecaseguard catches it. - A different but valid tarball is silent.
- A failed download is loud:
- What would satisfy me: add a
HEXAGON_TOOLCHAIN_SHA256besideQEMU_SRC_SHA256. Download to a file, check it withsha256sum -c, then extract. Optionally,grep -q 'clang version 19.1.5'on the--versionline.
2. [medium-low] docs/fft-design.md:2855-2864: the clean-room record understates what crossed from the port measurements.
- The record says the implementer received "counts as numbers only, per scenario, plus a per-transform figure", and was barred from "the orchestrator's scratch except the brief".
- The brief (
scratchpad/srdif-hex/BRIEF.md) actually carried more than that:- (a) per-transform-pair figures for both profiles: float 14.4 k vs 18.3 k and double 31.4 k vs 35.0 k at N = 512;
- (b) a size-localized comparison, "at N = 64 srdif was already 1–2 % below". This comes from measuring the port at a second size, and it tells the reader where the gap is;
- (c) permission to read
clean-engine/TARGETS.mdandclean-engine/targets/**; - (d) the statement that "the ratchet counts instructions, not packets", which the implementer later showed to be wrong.
- None of this is expressive content of the port, and I do not think it taints the work. But this section exists to be an exact disclosure, and "a per-transform figure" is not exact.
- What would satisfy me: list every figure that crossed, verbatim (a–b). Name the clean-engine allowance (c). Note (d).
3. [low] bench/README.md:160-163 and the predecessor cells (bench/README.md:230-235, docs/fft-design.md:2337-2358): the Hexagon offset comes from the process, not the machine.
- The count moves with the binary's path and the environment qemu-hexagon passes to the guest. The same
tap_dsp_icount_rfft_q15_512binary, on one machine, read:- 159,978,121 from one directory;
- 159,978,852 from a directory whose path is 44 characters longer (+731);
- 1,584 fewer packets under
env -ithan with the normal environment.
- "A few hundred to about a thousand between machines" is also low: the implementer's machine reads +1,723 against CI and mine +1,596.
- As a consequence, the "CI-equivalent" predecessor cells cannot be reproduced to the packet. I built
7a58ebewith its own harness, at the same path length as my head build: its fixed-point scenarios equal the head's, and its float scenarios read 28 packets below the cited 49,732,931 / 54,834,714 / 103,239,537, on every scenario. - The README's predecessor fixed-point cells (159,976,914 / 184,890,609 / 157,303,265) sit exactly 28 above #42's. That contradicts
fft-design.md's own premise that the fixed-point scenarios "run the same code on both trees". The 28 is an artifact of the orchestrator's build path or harness. - The percentages do not move: 28 packets is 6e-7.
- CI itself is stable: the PR run and the dispatch run agree to the packet.
- What would satisfy me: say "process environment and binary path" instead of "machine". State the ~1.6–1.7 k offsets actually observed. Either mark the predecessor cells as ±28 or give their fixed-point cells as equal to #42's, so the table does not imply a fixed-point difference.
4. [low] docs/fft-design.md:2342: "The Cortex-M counts reproduce bench/baselines.json exactly" holds for the float rows only.
- In the PR's own dispatch run 36363710480,
icount m33reads:rfft_q15_512758,994,136 against a baseline of 752,189,619 (+0.90 %);rfft_q31_2048+0.49 %;rfft_q31_512+0.69 %.
m4freads +0.61 … +0.90 %. I get the same numbers locally, and the same at28befbd. So this drift predates this PR (the fixed-point baselines were seeded at #32 and have not been re-recorded) and is not caused by it.- The PR body's "Arm rows confirmed … +0.00 %" is correct for the eight float rows it names.
- What would satisfy me: qualify the sentence, for example "the float rows reproduce exactly; the fixed-point rows read the same in-band drift as on
main". Filing a follow-up to re-record the fixed-point rows would be welcome, but not in this PR.
5. [low] include/tap/dsp/fft/srdif.h:716-722: mul_sub's bit-identity claim needs FLT_EVAL_METHOD == 0 as well as "without contraction".
g++ -m32 -mfpmath=387 -O2 -ffp-contract=off -fexcess-precision=standard -Son the two spellings:- the split form rounds
abto float (fstps/flds) before the subtraction; - the plain form keeps it in extended precision.
- the split form rounds
- So on an x87 build the output bits do move. The fingerprints are already scoped to
FLT_EVAL_METHOD == 0(astatic_assertin the test), so no pinned claim breaks; the docstring is simply broader than what holds. - What would satisfy me: add "and FLT_EVAL_METHOD == 0" to the sentence.
6. [low] CLAUDE.md:43: "forced inlining under clang for Hexagon" misdescribes the gate.
- The gate is
defined(__clang__), so the forcing applies to every clang: AppleClang (the double profile on macOS), clang-cl, IntelLLVM, and clang-based Arm toolchains (armclang, ATfE). - Only Hexagon and x86 were measured. I measured one more target to see whether the choice is right elsewhere: the M33 icount harness compiled with clang++ 18 (
--target=thumbv8m.main-none-eabihf -mcpu=cortex-m33 -O3) and linked with arm-none-eabi-g++ as the toolchain file does. Forced: 83,478,506 / 98,999,250. Not forced: 89,827,306 / 105,224,146. That is −7.1 % / −5.9 %, so the choice helps there too. - What would satisfy me: "forced inlining under clang (every target; measured on Hexagon)". Optionally add the clang-on-M33 figure to the
srdif.hcomment.
7. [low] bench.yml:208 and the "Toolchain versions" step: a missing qemu-hexagon passes both checks.
… -plugin help 2>&1 | grep -q "unknown option"does detect a build without plugins: linux-user printsqemu: unknown option 'plugin', which I checked inlinux-user/main.c. If the binary is absent, though, the output is "No such file or directory", grep finds nothing, and the step passes.qemu-hexagon --version | head -1then passes as well, because the default shell isbash -ewithoutpipefail(the job log confirmsshell: /usr/bin/bash -e {0}).- The job does fail in the end, but as a Python
FileNotFoundErrortraceback fromicount.py, not as an annotated error. - What would satisfy me:
test -x "$HOME/qemu-hexagon-plugins/qemu-hexagon" || { echo "::error::…"; exit 1; }before the probe, andset -o pipefailin that step. The plugin probe would also read better in its own step than inside "Download the Hexagon toolchain".
8. [nit] srdif.h:112: TAP_DSP_SRDIF_INLINE depends on __OPTIMIZE_SIZE__, which is decided per TU.
- A consumer that mixes
-Osand-O2TUs gets token-different definitions of the same inline members. This is benign in practice, since the mangling is unchanged. - But the one surviving weak
kernel<…>is whichever the linker keeps. "Not forced under -Os" is therefore a per-link outcome, not a per-TU guarantee: a size-built target can end up with the 12 KB forced kernel. - What would satisfy me: one sentence saying so.
9. [nit] Host test count. The PR says "Host Release 442/442". At this head I get 437/437 with g++ 13 and with clang 18 (default options plus TAP_DSP_WERROR=ON). Which configuration gives 442?
10. [nit] Cache scope. The qemu-hexagon cache saved by the workflow_dispatch run on the branch is not visible to refs/pull/44/merge: PR run 36364049440 rebuilt QEMU and saved again. Nothing is wrong. It will settle once main saves the cache, and the key (QEMU digest + -1) is sound as long as the configure line changes only together with a bumped suffix. A comment saying so would help the next editor.
The five Arm keys are unaffected by the workflow changes: matrix.arch is empty for them, so every Hexagon step is skipped, and SIZE_TOOL stays arm-none-eabi-size. Run 36363710480 confirms this.
Provenance: the tuned srdif.h against 7a58ebe:include/tap/dsp/fft/split_radix.h
None of the five changes moves srdif toward the port's structure, and three move it further away.
- Small blocks. The port handles them as register leaves:
cftf161andcftf081load all 16 or 8 complex values into scalars, then store them, as inx0r = a[0] + a[8]; … y5r = wn4r * (x0r - x0i); … a[8] = y1r + y5r;. Below that comecftf040andcftx020. The PR removes srdif's register leaves.level<L>loads, transforms and stores one butterfly(J, J+q, J+2q, J+3q)at a time, in memory, and the recursionblock<L/2>(a); block<L/4>(a+L); block<L/4>(a+L+L/2)goes down to pairs. The one intermediate step that looked like the port was "first level of 16 in memory, then register leaves of 8", the shape ofcftmdl1+cftf081, and it was not kept. - Post-pass. The port's
rftfsub/rftbsubhandle one pair per iteration, updating in place:a[j] -= yr; a[j + 1] -= yi; a[k] += yr; a[k + 1] -= yi;.post_twotakes two consecutive pairs, reads all eight values before any store, and computes through a value-returningpost_math. The port has no such form. - Run-time level. The port's
cftmdl1pairs j with its mirrorj0 = m - jin one iteration, swappingwk1randwk1i. The PR's double path now runs one group per step and no pairing at all. The float path keeps #42's j, j+1 pairing, which was already not mirror pairing. - Permutation. Unchanged, apart from
swap2being forced inline. There is no move towardbitrv2'sip[]table. mul_sub. The port writeswk1r * x0r - wk1i * x0ias a single expression. The split statement exists only to steer clang's contraction.- Scratch. I found no leftover Hexagon build product of the port in the session scratchpad (checked with
filefor Hexagon ELF objects newer than the brief). The Hexagon build of7a58ebethat I made for finding 3 is deleted with this review.
Generated by Claude Code
…cords bench.yml: - The CodeLinaro toolchain tarball is digest-verified (HEXAGON_TOOLCHAIN_SHA256, sha256sum -c before extraction), and its version is asserted, like the QEMU source and plugin header. - qemu-hexagon's plugin probe has its own step: a missing binary fails there, not later as an icount.py traceback. The version step sets pipefail. Records: - docs/fft-design.md, the clean-room record: everything that crossed to the implementer, verbatim. That is the port's scenario counts, the per-pair microbenchmark figures for float and double, and the N = 64 comparison. It also names the #42 harness allowance, and notes the brief's wrong "instructions, not packets". - The Hexagon offset belongs to the process (binary path and environment), not the machine: +731 for a longer path, -1,584 under env -i, and +473 to +1,723 observed against CI. The predecessor cells read +-28. The README's predecessor fixed-point cells now equal #42's (the same code). - "The Cortex-M counts reproduce exactly" is for the float rows. The fixed-point rows carry the pre-existing in-band drift from #32. - srdif.h comments only (no code change; fingerprints unchanged): - The forced-inline gate is every clang. The review's clang-18 Cortex-M33 measurement (-7.1 / -5.9 %) is cited. - The gate is per translation unit. - mul_sub's bit identity needs FLT_EVAL_METHOD == 0. - Hexagon figures are packets. - CLAUDE.md: "every clang, tuned on Hexagon". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy
…1e33 The rows #44 recorded left the main SHA pending until the squash. The push-to-main bench run on d9c1e33 (run 36367784465, compare mode) confirms them: - The eight Cortex-M float rows reproduce to the instruction. - The six hexagon rows read +22 packets on every scenario, fixed point included. That is the process-environment offset bench/README.md describes, inside the band, and the baselines are not re-recorded. The cells now name d9c1e33 and the run. No baseline or ceiling changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy
What this changes
hexagonkey for the instruction-count ratchet. Addscmake/hexagon-linux-musl.cmake, theicount.pytarget, and abench.ymljob that builds a plugin-enabledqemu-hexagonfrom the pinned QEMU 8.2.2 release. It uses the same CodeLinaro clang 19.1.5 toolchain and flags as MuTap's Hexagon leg.-ffp-contract=off.Why
MuTap's bump to
72977aameasured srdif +12–13 % on its Hexagon chain workloads (fdkf, shadow, suppressor, chain). The float FFT alone read +27–32 % at N ≥ 512. #42's "must not regress" bar was checked only on the Cortex-M keys, because no key here measured Hexagon. The maintainer chose to fix srdif before MuTap takes it.Verification
Instruction counts
The Hexagon counts come from the
workflow_dispatchruns on this branch: 36357132506 for #42's engine and 36363710480 for the tuned engine. The predecessor figure is a local measurement with the same harness, converted by the runner's constant offset..text: the f32 probes grow by 64–384 B and stay under every ceiling. The Hexagon ceilings are recorded new.What changed, and what paid. Details are in
docs/fft-design.md, "Hexagon (clang) tuning".-Os. clang had left the groups, leaf helpers and index-sequence lambdas as calls. Under GCC the same forcing costs the M4F / M33 / M55 keys 4–9 %, so GCC is left alone.-ffp-contract=off.The Hexagon key counts packets, not instructions. qemu-hexagon reports one guest instruction per VLIW packet, as a per-address profile showed.
bench/README.mdsays so.Output bits.
tap_dsp_srdif_fingerprintpasses unchanged:Builds that contract (the bench's default flags) move the float checksums on the FPU keys and on Hexagon float. The fp-contraction policy allows that.
Implementer's local runs
scripts/tidy.shclean.ci.ymlbuilds them.tap_dsp_tests423/426. The three failures are the dlopen ABI-tag tests, which a static musl binary cannot run.I independently re-measured the Hexagon counts at the head of the tuning commits.
Clean room. The tuning was written by an implementer barred from the port, the package, pre-hand-off history, the MuTap trees, the provenance records and every PR. The orchestrator, which had read the port, passed the port's Hexagon counts as numbers only, and deleted its own port build products first. The access statement is in
docs/fft-design.md, "The Hexagon tuning pass, clean-room". It includes single lines of forbidden sections that broadgreps printed: routine names and one-line prose, no code, reported unused.Notes for the reviewer
claude/dsptap-srdif-bump, held) will move to this PR's squash once it merges. That re-records MuTap's Hexagon ratchet and all nine fingerprint legs from one CI run.icount hexagon, once this is onmain.bench/README.md.🤖 Generated with Claude Code
https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy
Generated by Claude Code