ISA Quirks and Fixups
Concrete edge-case results, portable contracts, and the cost of fixing ISA differences
A portable vector type guarantees a lane count; it does not guarantee one machine
instruction, identical floating-point results, or availability under every token.
This page distinguishes uniform contracts we enforce from current differences
callers must account for. It covers the generic magetypes::simd::generic API,
primarily f32x4, f32x8, and f32x16; do not transfer these tables to legacy
architecture-specific wrappers or other conversion methods without checking them.
The floating-point tables assume the normal floating-point environment. Changing
rounding modes or enabling flush-to-zero can change results. NaN means a NaN
result, without a guarantee about its payload or sign.
Runtime fixups: exact calls and extra work
These are source-level lowerings, not universal cycle counts. Constants can fold away, invariant count preparation can move out of a loop, and wider polyfills repeat the native sequence. The measured table below identifies its CPU, width, compiler, baseline, and workload.
| Exact generic method call | ISA / shape | Baseline and runtime fixup | Where the extra work occurs |
|---|---|---|---|
v.to_i32_saturating() on f32x4, f32x8, f32x16 | V3 native 128/256; W512 uses two halves | to_i32() uses truncating conversion. Add an overflow comparison, select i32::MAX, unordered comparison, and AND-NOT to zero NaNs. | Four additional vector operations per native half, plus constant materialization if needed. No data-dependent branch in this lowering. |
| Same calls | Native V4/V4x f32x16 | Truncating conversion plus two mask comparisons and two masked moves. | Per vector; predicate-register form rather than V3's full-vector masks. |
| Same calls | NEON / WASM / scalar | The base conversion already clamps and maps NaN to zero. | No additional semantic repair; benchmark these as identity controls. |
v.recip() / v.rsqrt() on f32x4, f32x8; V3 f32x16 | V3 native half | Same estimate and Newton arithmetic, then unordered compare and blend back to the estimate on invalid intermediates. | Two semantic repair operations per native half; the inspected lowering also needs a register copy to retain the estimate. Constants, loads and scheduling affect time. |
v.recip() / v.rsqrt() on native f32x16 | V4/V4x | Estimate + Newton + VFIXUPIMM with a constant rail table. | One fixup instruction per vector plus table materialization; retaining values can also require register copies. |
v.recip() / v.rsqrt() | NEON f32 | Two fused refinement steps. rsqrt places a and y*y inside FRSQRTS instead of forming the invalid a*y first. | Operand arrangement preserves special cases without an added compare/select. Current probe uses an identity control here, not the historical broken arrangement. |
v.shl_uniform(count) / v.shr_logical_uniform(count) on i16x8/16/32, u16x8/16/32, i32x4/8/16, u32x4/8/16 | NEON | Clamp count to lane bits before conversion/broadcast; negate for right shift. This prevents large counts aliasing through NEON's signed low-byte interpretation. | Scalar min/select and broadcast per distinct count; usually hoistable when all loop iterations share the count. |
v.shr_arithmetic_uniform(count) on signed shapes above | NEON / WASM | Clamp to lane bits minus one, preserving sign-fill for excessive counts. | Scalar count preparation; NEON also broadcasts and negates. |
v.shl_uniform(count) / v.shr_logical_uniform(count) on the shapes above | WASM | Native shifts mask counts modulo lane bits. Compare the original count and AND the result with all-ones or zero. | Count comparison/splat plus a vector AND; invariant preparation may hoist. |
| All uniform-shift calls above | x86 | Native excessive-count zero/sign-fill behavior already matches. | No semantic repair. Moving a runtime count into a register is still required. |
a.narrow_saturating_i8(b) / a.narrow_saturating_u8(b) on i16x16; a.narrow_saturating_i16(b) / a.narrow_saturating_u16(b) on i32x8 | AVX2 | Native packs interleave 128-bit groups. VPERMQ with 0xD8 restores all of a followed by all of b. | One lane-order permutation per 256-bit pack; the V3 W512 path repeats it twice. |
Same narrowing methods on i16x32 / i32x16 | Native AVX-512 | Narrow each input to a half and insert/concatenate. Unsigned destinations clamp signed inputs to zero before unsigned conversion. | Two zero clamps for unsigned output, plus the two conversions and concatenation needed for the operation. |
| Same narrowing families at 128 bits; NEON / WASM widths | Native halves | Native signed-source saturating narrows already have the desired lane order. | No AVX2-style lane-order repair; polyfills still require composition. |
v.shl_const::<N>(), v.shr_logical_const::<N>(), v.shr_arithmetic_const::<N>() | Every backend | Const assertions reject invalid counts before execution. | No runtime assertion. Constant byte shifts can still require shift/mask emulation. |
a.mul_add_fused(b, c) / a.mul_sub_fused(b, c) (also spelled mul_add_portable / mul_sub_portable) on every float type | Scalar backend; WASM | Native hardware backends use FMA directly. The scalar backend uses the FMA instruction when the build guarantees one (and for x86_64 f64 with std, when a cached runtime check finds it), else software. Relaxed-SIMD WASM uses relaxed madd when a cached probe shows the engine fuses it, else software; strict WASM always uses software. Software f32 rounds once through f64 (WASM SIMD f32 rounds to odd); software f64 uses a double-double product. Both check rare lanes into an exact path. | The software forms have no per-lane branch, so they vectorize on SSE2 and WASM SIMD. Two cached checks run once per call: the x86_64 std f64 CPU flag, and the relaxed-WASM fusion probe (per element type). Nothing else detects. Measurements are below; see Fused arithmetic. |
abs_diff, madd_adjacent, pairwise_widen_add, saturating arithmetic, and
reduce_add_u32 sometimes need several instructions because an ISA lacks that
operation. That is emulation of the operation, not an extra check imposed by a
safe wrapper. round() enforces ties-to-even; the scalar fallback may need
rounding code while hardware backends have native rounding instructions.
Measured overhead and recorded values
Open the interactive ISA explorer to filter by CPU, ISA, width, and exact call; inspect decimal values and raw bits; or download the JSON with every paired timing round. It is an offline record viewer and does not claim to execute NEON or AVX-512 in your browser.
The first measurements use 2,048 ordinary positive f32 values, release builds,
no target-cpu override, and nine alternating AB/BA rounds. Timings include
loads, stores, and the loop. The percentage is the median of paired ratios;
MAD describes within-process variation, not a confidence interval. These are
throughput costs in this kernel, not instruction latencies or predicted
application slowdowns. Different widths cannot be compared as equal work per
vector. Exceptional inputs are sampled separately, not included in timing.
| CPU / compiler | Exact call / shape | Baseline | Median overhead | Ratio MAD |
|---|---|---|---|---|
| AMD Ryzen 9 7950X 16-Core Processor / 1.98.1 | to_i32_saturating() / x86-v3/f32x4 | to_i32() | +84.2% | 0.6 pp |
| AMD Ryzen 9 7950X 16-Core Processor / 1.98.1 | recip() / x86-v3/f32x4 | same Newton arithmetic, no rail repair | +31.8% | 1.6 pp |
| AMD Ryzen 9 7950X 16-Core Processor / 1.98.1 | rsqrt() / x86-v3/f32x4 | same Newton arithmetic, no rail repair | +26.7% | 0.5 pp |
| AMD Ryzen 9 7950X 16-Core Processor / 1.98.1 | to_i32_saturating() / x86-v4/f32x16 | to_i32() | +44.7% | 0.7 pp |
| AMD Ryzen 9 7950X 16-Core Processor / 1.98.1 | recip() / x86-v4/f32x16 | same Newton arithmetic, no rail repair | +17.2% | 0.5 pp |
| AMD Ryzen 9 7950X 16-Core Processor / 1.98.1 | rsqrt() / x86-v4/f32x16 | same Newton arithmetic, no rail repair | +13.7% | 1.1 pp |
| Apple M4 Pro / 1.98.0 | to_i32_saturating() / neon/f32x4 | identity control | -0.0% | 1.2 pp |
| Apple M4 Pro / 1.98.0 | recip() / neon/f32x4 | identity control | -0.0% | 0.1 pp |
| Apple M4 Pro / 1.98.0 | rsqrt() / neon/f32x4 | identity control | -0.1% | 0.1 pp |
The M4 identity controls show measurement variation, not a benefit from adding
or removing code. Integer clamp/permutation costs above are currently supported
by source/codegen inspection; isolated baseline-versus-fixup timings have not
yet been recorded for them. Existing int_uniform_shift benches compare
runtime versus constant counts, which also changes instruction selection and
is not an isolated fixup measurement. Do not label that ratio a clamp cost.
Reproduce the sample/timing run with cargo run --release -p magetypes --example isa_fixups --features avx512. Use -- --samples-only for Wasmtime/QEMU. Capture
rustc -Vv and CPU identification before stdout, then collect runs with
python3 xtask/isa_evidence.py CPU=run.txt --output docs/site/static/isa-explorer/data.json.
The collector validates array shapes, bit encodings, and saturating-conversion
outputs against an independent scalar oracle. The dataset records its library
revision and probe source hash. Store actual outputs per CPU; do not synthesize
ISA results from a table or report emulator timings as hardware performance.
Results we make portable
| Operation and input | Result on every backend | What we fix up |
|---|---|---|
shl_uniform(16) / shr_logical_uniform(16) on 16-bit lanes; likewise count 32 on 32-bit lanes | All zero | x86 count behavior already agrees. NEON clamps before broadcasting the count; WASM masks the shifted result. |
shr_arithmetic_uniform(16) on signed 16-bit lanes [1, -1, MIN, MAX] | [0, -1, -1, 0] | NEON/WASM clamp the count to 15. Counts as large as u32::MAX obey the same contract. |
*_const::<0>() | Input unchanged | NEON supports the zero-count case through the chosen lowering. |
*_const::<lane_bits>() or a negative constant count | Compile error | A front-end const assertion prevents ISA-dependent masking, zeroing, or rejection. No runtime check. |
u8 saturating addition 250 + 10 | 255 | Native saturation on hardware backends. Scalar implements the same contract. |
u8 saturating subtraction 3 - 10 | 0 | Same as above. Saturating add/sub are exposed for 8/16-bit lanes. |
Widen unsigned byte 255 / signed byte -1 | 255u16 / -1i16 | Signedness selects zero/sign extension. Low/high select the corresponding half of the source lanes. |
Narrow signed i16 values [-1, 0, 255, 256] to u8 | [0, 0, 255, 255] | Unsigned destinations still have signed sources. Native x86/WASM packs interpret sources that way. |
Narrow two vectors a, b | All clamped lanes of a, followed by all clamped lanes of b | AVX2 packs interleave 128-bit groups; a permutation restores order. Native AVX-512 uses conversions and insertion, plus a zero clamp for unsigned destinations. |
Signed i16 MIN.abs_diff(MAX) | 65535u16 | NEON has native absolute difference. x86/WASM use signed max/min and wrapping subtraction; the unsigned result preserves the full range. |
Adjacent i16 dot product with both pairs equal to (MIN, MIN) | i32::MIN | madd_adjacent wraps modulo 2³². NEON widens products and pair-adds; x86/WASM dot instructions already wrap this exceptional sum. |
pairwise_widen_add on unsigned pairs (255u8, 255) / (65535u16, 65535) | 510u16 / 131070u32 per pair | Both inputs widen before addition. NEON/WASM have unsigned pairwise widening instructions; x86 masks and shifts each wider lane before adding. |
| Sum 64 bytes all equal to 255 | 16320u32 | reduce_add_u32 widens rather than using wrapping byte addition. sum_abs_diff has the same maximum for 64 lanes. |
round() on [2.5, 3.5, -2.5, -3.5] | [2, 4, -2, -4] | Ties-to-even, including the scalar fallback. This differs from Rust's ties-away-from-zero f32::round(). |
Runtime byte shifts are deferred; constant byte shifts remain available. x86 implements constant byte shifts with wider shifts and masks because it has no native byte-shift instruction. No new runtime fixups are introduced by the trait consolidation.
Float-to-integer conversion: choose the contract
to_i32 truncates finite, in-range values. Its name does not promise Rust's
saturating as i32 behavior for exceptional inputs. to_i32_saturating does.
| f32 input | to_i32: x86 | to_i32: NEON / WASM / scalar | to_i32_saturating: all |
|---|---|---|---|
1.9 / -1.9 | 1 / -1 | 1 / -1 | 1 / -1 |
3_000_000_000.0 | i32::MIN | i32::MAX | i32::MAX |
+inf | i32::MIN | i32::MAX | i32::MAX |
-inf | i32::MIN | i32::MIN | i32::MIN |
NaN | i32::MIN | 0 | 0 |
The saturating variant adds compare/select fixups on x86; NEON/WASM/scalar already
have the requested conversion behavior. A caller that has proved the input range
may prefer to_i32. This table does not describe to_i32_round, f64 conversions,
or pixel-packing helpers.
Current floating-point differences we do not fix up
These are observable implementation differences, including differences between widths on the same backend. They are compatibility constraints to review, not recommendations to depend on unusual NaN behavior.
| Expression | x86 V3, all supported widths | Native V4/V4x f32x16 | NEON / WASM | Scalar f32x4/f32x8 | Scalar f32x16 |
|---|---|---|---|---|---|
min(NaN, 1) and max(NaN, 1) | 1 | 1 | NaN | 1 | 1 |
min(1, NaN) and max(1, NaN) | NaN | NaN | NaN | 1 | NaN |
simd_ne(NaN, 1) and simd_ne(NaN, NaN) | false mask (ordered comparison) | true mask (unordered comparison) | true mask | true mask | true mask |
Negate +0.0 | +0.0 | +0.0 | -0.0 | -0.0 | -0.0 |
Negate -0.0 | +0.0 | +0.0 | +0.0 | +0.0 | +0.0 |
x86 min/max select the second operand when a NaN is involved. NEON uses
vminq/vmaxq, and WASM uses min/max, which propagate NaNs. Smaller scalar
vectors use Rust's numeric min/max; scalar f32x16 currently uses comparisons and
selects the second operand when the comparison is false. Rust's numeric min/max
ignore a single NaN and do not promise which equal signed zero is returned.
The scalar contract is documented by Rust.
For an unordered not-equal mask, invert simd_eq; do not assume simd_ne currently
has identical NaN behavior on every tier. If NaNs or signed zeros matter to min/max
or negation, handle them explicitly. Follow-up candidates are an unordered x86
comparison predicate and sign-bit XOR negation; both need their own semantic and
codegen review rather than silently changing a published operation here.
Fused arithmetic and reductions
| Example | x86 / NEON | WASM / scalar | What we fix up |
|---|---|---|---|
a.mul_add_relaxed(b, -1) where a = 1 + 2^-23, b = 1 - 2^-23 | -2^-46 (fused) | 0 (multiply, then add); relaxed WASM: either | Nothing: mul_add_relaxed fuses only where the hardware does. |
a.mul_add_fused(b, -1), same inputs | -2^-46 (fused) | -2^-46 (fused in software) | Software single rounding where the hardware cannot fuse. |
Floating-point reduce_add | Association depends on backend and vector shape | Association depends on backend and vector shape | Nothing: no universal cross-backend ULP or relative-error bound. |
mul_add_relaxed and mul_sub_relaxed round once on x86 v3/v4 and NEON, and twice (multiply,
then add) on the scalar backend and strict WASM; relaxed WASM follows the
engine's rounding choice. mul_add_fused and mul_sub_fused round once on
every backend, so their results match bit for bit. NaN payloads and signs remain
unspecified. One and two roundings can differ near cancellation or intermediate
overflow.
On relaxed WASM, mul_add_relaxed emits relaxed multiply-add directly, without a
probe, cached load, or runtime branch. A relaxed-SIMD token proves instruction
availability, not fusion: the
relaxed-SIMD specification
permits either rounding behavior, but requires each environment to make one
fixed choice per operator. mul_add_fused relies on that: on first use it
evaluates one input whose fused and unfused results differ, caches the answer
per element type, and then uses relaxed madd on engines that fuse and the
software path on engines that round twice. The result is the same either way.
The probe and the f32 software path are inline with no call site, and the f64
software path sits behind a single call: any call site in a loop made
wasmtime keep loop state in memory, even when the call never ran. With
wasmtime 40 on a Ryzen 9 9950X3D, a fusing engine runs f32x4 in 0.08× (chain)
and 0.15× (stream) of the software path's time, within 5–45% of bare relaxed
madd; engines that round twice measured the same as plain software
(layout experiment).
V8 (node 26.7.0) made the same choice in its baseline and optimizing tiers,
with and without host FMA
(tier check). Modules built with relaxed SIMD still require
an engine that supports those instructions; the arithmetic fallback does not
make such modules loadable on older engines.
V8's x86 QFMA lowering selects hardware FMA when the CPU exposes FMA3, otherwise separate multiply and add (source checked 2026-09-27). This choice occurs during code generation, not as a per-operation branch in the generated kernel. Thus relaxed-SIMD support alone does not imply fusion on older CPUs or VMs that hide FMA.
Single rounding has a price where the hardware cannot fuse, and mul_add_relaxed does
not pay it. Measured against a * b + c on 1,024 L1-resident vectors (stream)
and on 1,024 dependent steps (chain); records:
per backend,
WASM.
| Backend, machine | mul_add_relaxed | mul_add_fused |
|---|---|---|
| AVX2 and AVX-512, Ryzen 9 9950X3D | Chains take 29–38% less time; streams −10% to +6%, within code-placement noise | The same instruction as mul_add_relaxed |
| NEON, Apple M4 Pro | Chains take 24–31% less time; streams −5% to +8% | The same instruction as mul_add_relaxed |
Historical scalar hardware path (removed before 0.9.31-beta; AArch64 always; x86-64 built with -Ctarget-cpu from Haswell/Zen 1 on), Neoverse-N1 and 9950X3D | The same code as a * b + c | The same instruction as mul_add_relaxed: 0.92–1.03× in streams, 0.65–0.78× in chains (record) |
| Historical scalar cached-dispatch path (removed before 0.9.31-beta), default x86-64 build on 9950X3D | The same code as a * b + c | A cached check and a call per lane: 2.7–3.0× (f64), 2.9–5.7× (f32) the time |
| Scalar software algorithm (measured on the 9950X3D and M4 Pro before the hardware experiment) | The same code as a * b + c | 2.7–15.5× (f32), 9.1–29.5× (f64) the time |
| Historical WASM SIMD128 (per-lane f64), wasmtime on the 9950X3D | The same code as a * b + c | 8.2× (f32x4), 23× (f64x2) the time |
| Historical WASM relaxed SIMD, same | The engine's madd: 0.96–1.06× the time | 8.2× (f32x4), 24× (f64x2) the time |
| Scalar software, array form (current), baseline x86-64 on the Ryzen 9 7900X | The same code as a * b + c | 1.7–8.8× (f32), 5.4–27× (f64) the time; inside a V3 function 3.8–4.3× (f32), 7.1–8.4× (f64) (record) |
Scalar f64 with the cached runtime check (current), x86_64 std on the 7900X | The same code as a * b + c | 0.86–6.6× the time (one call per lane); inside a V3 function 0.67–1.0×, where the call inlines |
| WASM SIMD128 (current), wasmtime 47 on the 7900X | The same code as a * b + c | 7.9–11× (f32x4), 9.2–13.5× (f64x2) the time |
| WASM relaxed SIMD (current), wasmtime 40 on the 9950X3D | The engine's madd: 0.65–1.0× the time | Fusing engine: 0.68–1.45× (f32x4), 1.6–2.1× (f64x2); engine that rounds twice: 8.9–10× (f32x4), 10.4–11.3× (f64x2) the time (record) |
Portable software fallbacks work on whole lane arrays without a branch per lane, so LLVM vectorizes them. f32 computes the exact product in f64, adds once and narrows; a lane goes to the exact round-to-odd path only when the f64 sum is halfway between two f32 values or lies in f32's subnormal range, the only cases where narrowing can round differently. f64 uses Dekker's product and TwoSum, rounds to nearest, and sends lanes whose final addition lands exactly halfway after an inexact inner addition, or that leave the safe exponent range, to an exact integer implementation ported from libm 0.2.16. They do not probe the CPU.
Scalar helpers use the FMA instruction when the build guarantees it: with
std, AArch64 NEON or x86 FMA-enabled builds use Rust's mul_add; without
std, V3 or NEON target features let a compile-time token reach the
intrinsic. For f64 on x86_64 with std, a cached runtime check (archmage's V3
token, once per call) also selects the instruction on CPUs that have it: on a
Ryzen 9 7900X and a Core Ultra 7 265K that took 0.09–0.43× the software path's
time. f32 stays in software without a compile-time guarantee, because its
vectorized software path measured as fast as a per-call hardware path or
faster. Results are identical on every path. Native vector backends retain
their token-proven FMA intrinsics. Strict WASM portable operations use
software; relaxed WASM uses the engine's madd when the cached probe shows it
fuses. The bit-exact contract assumes round-to-nearest and gradual
underflow; NaN payloads and signs are unspecified.
CI checks committed expected bits generated with Python exact rational
arithmetic, independently of std/libm, alongside the broader randomized suite.
The contract runs on native x86/ARM, cross targets, WASM and Miri, in std and
no_std configurations. Bare arithmetic is checked separately against its
backend's fused, unfused, or relaxed behavior.
The scalar rows above measure earlier implementations. Before the portable software rounding fixes, an assembly comparison with Rust 1.99 checked the f32/f64 helpers and portable add/sub at all six scalar vector widths: each matched the corresponding direct libm expression on baseline x86-64, x86-64-v3, AArch64, and WASM after normalizing local label numbers. That establishes code-generation equivalence for those fixtures, not a runtime speedup. Native WASM SIMD's f32 software kernel still rounds to odd. The scalar helpers now use the array algorithm above and no longer match direct libm. See the relaxed SIMD investigation for the independent counterexample and measured distinction between bare relaxed madd and portable software fusion.
On a native FMA backend, mul_add_relaxed is one FMA: the faster choice for dependency
chains through the multiplicand, such as Horner polynomials. When the chain runs only
through the addend (a reduction with one accumulator), a separate a * b + acc takes
0.49–0.80× the time of FMA on Arrow Lake and Zen 4, because the add finishes sooner than
an FMA; with enough independent accumulators the two are equal
(measurements;
loop patterns and examples:
Reductions).
Use mul_add_fused where identical results on every backend matter more than speed.
mul_add and mul_sub are deprecated so that backend-dependent rounding is an explicit
choice; in magetypes 0.11 they become the fused operations.
The generic transcendentals (exp2_midp, log2_midp, pow_midp and the rest)
evaluate their polynomials with mul_add_relaxed, so their last bits depend on the
backend. Over every f32 input, 0.3–0.8% of exp/log results differ by 1–3 ULP
between the scalar backend and AVX2, and about 11% of pow_midp(2.4) results
by up to 178 ULP; the maxima against the true value move by at most 0.43 ULP
(record).
Floating-point reductions retain backend-dependent association. Single-rounding multiply-add does not make an entire algorithm independent of reduction order.
Reciprocal and reciprocal square root
| Input | recip() | rsqrt() | Contract and fixup |
|---|---|---|---|
+0 / -0 | +inf / -inf | +inf / -inf | Working tier preserves IEEE special cases. |
+inf | +0 | +0 | x86 repairs invalid Newton intermediates; NEON uses fused refinement instructions with suitable special-case behavior. |
-inf | -0 | NaN | Same policy. |
NaN | NaN | NaN | Payload/sign unspecified. |
| Ordinary normal inputs in the tested range | Working precision target: at most 4 ULP | Working precision target: at most 4 ULP | Estimates plus refinement where appropriate; see the documented gap below. |
| Subnormal inputs | Unspecified at the working tier | Unspecified at the working tier | Use the _portable variants when subnormals matter. |
Observed contract gap: on Ryzen 7950X, V3 f32x4::recip() maps
f32::MAX (0x7f7fffff) to +0 and -f32::MAX to -0. Their correctly rounded
reciprocals are nonzero subnormals (0x00200000 / 0x80200000). Thus the current
rustdoc promise that only subnormal inputs are unspecified is too broad:
normal inputs with subnormal reciprocal outputs can also lose the ≤4 ULP
claim. This is recorded as a gap, not silently treated as a conforming result.
recip_portable() supplies the division result. A repair to the working tier
needs a separate accuracy/codegen/performance review; this documentation change
does not add runtime branches or alter the implementation.
On x86 128/256-bit vectors, special-case repair uses comparison and blending;
native AVX-512 uses fixup instructions. NEON arranges fused refinement to avoid
forming an invalid intermediate multiply. The _portable variants use division
(and square root for rsqrt). Raw _approx variants have weaker guarantees;
do not infer their special-case behavior from the working-tier table.
Transcendental approximation domains and fixups are documented separately in
Transcendentals.
Portability of cost and availability
| Abstraction | What can differ | How to use it |
|---|---|---|
| Fixed lane count | A 512-bit vector is one native AVX-512 register, two AVX2 registers, or four NEON/WASM vectors. | Treat width as a logical shape; measure the whole kernel. |
| Token feature superset | V4 does not implement every smaller vector backend just because the CPU supports their instructions. | Use the supported common shape, or explicitly downcast to V3 for a smaller-width kernel. |
| Widening/narrowing | Extraction, concatenation, lane-order repair, and load folding vary by ISA and shape. | Do not assume one instruction per public method. |
pairwise_widen_add followed by vector addition | NEON can fuse the pairwise sum and accumulation into UADALP; other ISAs use their own lowering. | Each pair sum is exact, but later accumulation wraps at the destination width. Bound the accumulated range or periodically widen/drain the accumulator. |
sum_abs_diff | x86 SAD produces partial sums natively; the public method returns a completed scalar reduction. | For long loops, vector accumulation followed by one reduction can beat reducing every chunk. |
| Checked slices and indexing | Unknown dynamic indices retain bounds checks; slice views may require length/alignment checks. | Use fixed arrays or loops whose bounds are visible to the compiler. Bounds checks are not unconditionally elided. |
| Unsigned-source narrowing, wider saturation, per-lane shifts | Some ISAs need emulation. Their absence from this API is a scope/cost decision, not mathematical impossibility. | Add an operation only with a concrete consumer and measured lowering. |
Executable evidence
The concrete floating-point examples are asserted in the isa_quirks module of
magetypes/tests/doc_examples.rs, across scalar, x86, NEON, and WASM runners.
Integer contracts are covered by int_widen_narrow.rs,
int_uniform_shift_saturating.rs, and the constant-shift boundary tests.
convert_saturating.rs, precise_reciprocals.rs, and sigmoid_silu.rs cover their
respective fixups. xtask/codegen.py checks per-ISA consumer assembly, direct
storage access, and integer formulations. These gates check the tested shapes
and contexts; they do not establish optimal codegen for every possible caller.