API reference — quiver/arith.h (K9 + K10)¶
Purpose. K9 — elementwise wrapping/IEEE arithmetic with validity composition; K10 — overflow-guarded arithmetic that is never silent UB (Charter §7.4). Roofline classes: K9 is bandwidth-bound at large n; K10-checked is compute-bound (PRD 08 §4). Introduced: v0.4. Stability: 0.x-fluid until the v1.0 freeze.
All functions inherit the common contract: noexcept, allocation-free, borrowed views only, thread-compatible pure functions; contract violations assert in debug builds and are UB in release; in-contract inputs are memory-safe and sanitizer-clean (PRD 04 §2, 16).
Contract¶
| API | Semantics |
|---|---|
arith(op, a, b, out) / arith(op, a, b_scalar, out) |
integers wrap (two's-complement/modular — REQ-K9-001, no UB path: unsigned internal arithmetic); floats native IEEE-754 |
arith(op, a, b, a_validity, b_validity, out, out_validity) |
defined as the composition: values at all lanes (invalid lanes hold defined-behavior garbage the validity bit masks) + mask_combine(kAnd) on validities — implemented via K4's public API (the documented cross-family exception, PRD 02 §6) |
arith_checked(op, a, b[, b_scalar], out, overflow_bits) → count |
wrapped results at all lanes; bit i of the nullable overflow_bits (capacity ⌈n/8⌉, tail-zeroed) set iff lane i overflowed; returns the exact overflow count (ADR-014); integers only |
arith_saturating(op, a, b[, b_scalar], out) |
clamps exactly at numeric_limits<T>::min()/max() (REQ-K10-002); integers only |
op ∈ {kAdd, kSub, kMul}. Overflow positions compose with K2/K3 for extraction — filter the batch by overflow_bits to retrieve the offending lanes (the ADR-014 design reuses the library's own vocabulary instead of inventing an error type).
ADR-014 (overflow reporting). Count + optional position bitmap was selected over a boolean flag (callers needing positions would rerun scalar) and over first-overflow-index (forces an early-exit data-dependent branch into the hot loop). The count accumulates branchlessly — cost is flat in overflow density (the benchmark's overflow_density axis is the evidence).
Capacity contracts: PRD 06 §6. Aliasing: exact aliasing out == a.data/b.data permitted; out_validity may alias an input validity (ADR-023).
Scalar reference (the specification)¶
K9's semantics are defined by src/kernels/arith/arith_scalar_impl.h (wrapping via unsigned internal arithmetic — no signed-overflow UB anywhere, PRD 15 §3); K10's by src/kernels/arith_guarded/arith_guarded_scalar_impl.h (sign-trick overflow detection for add/sub; widening multiply below 64-bit; 64-bit multiply checked via __int128 where available with a portable division-based fallback).
Per-ISA notes¶
- scalar (v0.4): the references above;
INT_MINnegation/multiplication edge cases are enumerated in the unit boundary matrix (REQ-K10-002). - AVX2 (v0.4): K9 add/sub/mul direct per lane width; 64-bit multiply via the same three-
vpmuludqdecomposition as K7 (PRD 08 §5). K10 checked add/sub vectorized with the sign-trick (overflow ⇔ sign(result) differs from sign(a) while signs of a,b agree — branch-free lane masks feeding a popcount accumulator); saturating add/sub native (vpadds*/vpsubs*) for 8/16-bit lanes, compare+blend clamp for 32/64-bit. All checked/saturating multiplies delegate to the scalar core — REQ-K10-003 permits this and this page plus the ledger state it plainly: 64-bit checked multiply has no profitable SIMD form on this ISA (the 128-bit high half is scalar-only), and the narrow widths follow the same shape for cross-ISA structural identity. - NEON (v0.4): K9 direct per lane width; 64-bit multiply via
umull/umlaldecomposition. K10 checked add/sub sign-trick vectorized; saturating add/sub native (vqadd/vqsub) at all widths — one of NEON's genuine conveniences over AVX2. Checked/saturating multiplies delegate to the scalar core (REQ-K10-003, same rationale as AVX2). - AVX-512 (v0.5): K9
arithruns direct vertical add/sub/mul per lane width; the 64-bit multiply is a nativevpmullq(_mm512_mullo_epi64, AVX-512DQ) instead of AVX2's 3×vpmuludq; 8-bit multiply widens even/odd bytes and repacks; floats native IEEE. Base set F+BW+DQ+VL only (correct on SDE-skx); bit-identical to scalar, validated (incl. NaN-class) under Intel SDE. K10arith_guardedAVX-512 backend lands later in the M7 rollout.
Ledger¶
Verdict (Apple M2, v0.4, neon vs autovec):
- K9
arith(8-byte types): the handwritten loop lost (~0.90×), so it now delegates to the autovectorized scalar reference. For pure elementwiseadd/mulon 8-byte elements (i64/u64/f64) the family is bandwidth-bound and the compiler's autovectorized scalar loop beat the explicit NEON loop (tighter tail and scheduling). Acting on that committed evidence, the shipped 64-bitarithpath delegates to that reference and runs at theautovecnumbers (REQ-KERNEL-007, evidence-gated backend choice). Narrower widths keep their handwritten NEON (not measured, not swept). The 0.90× rows below are the handwritten measurement that motivated the change. See the Apple M2 NEON investigation. - K10
arith_checked— neon wins 1.42×, and is FLAT across overflow density. The threechecked_addrows at 0 / 0.1% / 50% overflow are within 0.4% of each other — the direct measurement of ADR-014's branch-free-accumulation claim (cost does not scale with overflow count). - K10
arith_saturating— neon wins 1.65× via nativevqaddversus the autovectorized compare-and-clamp.
| api | configuration | neon vs autovec | entries |
|---|---|---|---|
arith add |
i64 n=65536 | 0.90× (loss) | qle:apple-m2-20260704-883c08552f35-e-bm-arith-add-neon-i64-n-65536-65536 qle:apple-m2-20260704-883c08552f35-f-bm-arith-add-autovec-i64-n-65536-65536 |
arith mul |
f64 n=65536 | 0.90× (loss) | qle:apple-m2-20260704-883c08552f35-e-bm-arith-mul-neon-f64-n-65536-65536 qle:apple-m2-20260704-883c08552f35-f-bm-arith-mul-autovec-f64-n-65536-65536 |
arith_checked add |
i64 n=65536 / ovf=0 | 1.42× | qle:apple-m2-20260704-883c08552f35-b-bm-arith-guarded-checked-add-neon-i64-n-65536-ovf-0-65536-0 qle:apple-m2-20260704-883c08552f35-b-bm-arith-guarded-checked-add-autovec-i64-n-65536-ovf-0-65536-0 |
arith_checked add |
i64 n=65536 / ovf=0.1% | 1.42× | qle:apple-m2-20260704-883c08552f35-b-bm-arith-guarded-checked-add-neon-i64-n-65536-ovf-1-65536-1 qle:apple-m2-20260704-883c08552f35-b-bm-arith-guarded-checked-add-autovec-i64-n-65536-ovf-1-65536-1 |
arith_checked add |
i64 n=65536 / ovf=50% | 1.42× | qle:apple-m2-20260704-883c08552f35-b-bm-arith-guarded-checked-add-neon-i64-n-65536-ovf-500-65536-500 qle:apple-m2-20260704-883c08552f35-b-bm-arith-guarded-checked-add-autovec-i64-n-65536-ovf-500-65536-500 |
arith_saturating add |
i64 n=65536 | 1.65× | qle:apple-m2-20260704-883c08552f35-g-bm-arith-guarded-saturating-add-neon-i64-n-65536-65536 qle:apple-m2-20260704-883c08552f35-g-bm-arith-guarded-saturating-add-autovec-i64-n-65536-65536 |
The arith (K9) neon medians come from a 4 s window and the autovec medians from an 8 s window — both CV-screened; ns_per_batch medians are window-length independent, and the longer autovec window was needed to bring this fanless secondary platform's streaming baseline under the 5% CV policy (REQ-LEDGER-005; several 2 s attempts were excluded). Apple M2 is a secondary platform (no_pmu, secondary; the only registered machine — ≥2-µarch is an open deferral, gate M6). Reproduction: disputes guide.
Validation¶
tests/unit/test_arith.cpp (boundary matrix × 8 integer types incl. INT_MIN cases) · tests/property/prop_arith.cpp (wrap/checked/saturate laws + K9-002 composition property, REQ-K9-002) · tests/differential/diff_isa_arith.cpp (backends vs the wide-math oracles, byte-exact) · tests/fuzz/fuzz_arith.cpp (K9+K10 differential) · bench/micro/bench_arith.cpp + bench_arith_guarded.cpp (overflow_density axis; hypotheses in-source).
Traceability: REQ-K9-001..002, REQ-K10-001..003, REQ-KERNEL-; ADR-006/-014/-016/-023; PRD 04 §5, 08 §5.*