simd::Lanes[T] at the Build’s Width, simd::Fixed[T, N] Where an Algorithm Demands a WidthStatus: Accepted provisionally (owner’s direction, October 20, 2026: vector types are wanted; this record decides them for a prototype, and is confirmed or changed from its measurement)
Date: October 20, 2026
Answers: ADR-371 D4’s questions 1 to 3 (#755): the type, its width, its
operations and the mask
Related: ADR-371 (portable lanes are std’s, an instruction of
one architecture a safe function of its backend), ADR-375 (a search
over a whole buffer), ADR-320 D7 (a read checked as the cut it
reads), ADR-296 D6 (no vector in the core)
The fastest 1BRC program written by a person parses the temperatures of two
rows in one 256-bit vector: a load, a shuffle that puts each row’s digits in
place, a multiply that adds neighbouring pairs, a reduction. Nikaia’s
find_all already hides a vector search inside std; a program that wants
its own vector code has nothing to write it with.
Other languages:
Vector<T> at the machine’s width, Vector128<T>/Vector256<T>
fixed, both safe; masks are vectors.std::simd::Simd<T, N>, fixed N, nightly only; stable Rust
writes core::arch intrinsics in unsafe.VectorSpecies.PREFERRED at the machine’s width, fixed species
beside it. Highway (C++): ScalableTag and FixedTag.@Vector(n, T) in the core, fixed n. Mojo: SIMD[DType, n].Two forms recur: one at the target’s width for loops whose result does not depend on it, one at a fixed width for algorithms defined over a width.
simd::Lanes[T] holds as many lanes as the build’s widest register.T is one of u8, i8, u16, i16, u32, i32, u64, i64.
Lanes[T].count is a constant of the build: the width of the CPU level’s
widest integer vector register divided by T’s size (16 bytes at x86-64
and on aarch64, 32 at x86-64-v3). The owner rejected a fixed width as the
main form (#755): it gives up the target’s best.
simd::Fixed[T, N] holds N lanes on every target.N is a literal power of two from 2 to 64; another is NK1297. A target
narrower than N lanes runs it in several registers, or lane by lane. It is
for algorithms defined over a width: a row parsed in 16 bytes, a 16-entry
table, a hash over blocks.
Lanes.On both: a load from a Bytes (load(b, at), checked as the cut
b[at..<at + count], so a proof drops it), splat(x), the lane-wise
wrapping_add, wrapping_sub, wrapping_mul, saturating_add,
saturating_sub, min, max, & | ^ !, << and >> by a constant, the
comparisons eq ne lt le gt ge answering a Mask, select(mask, a, b), and
the reductions wrapping_sum, min_lane, max_lane.
On Fixed only, because their result depends on the width: lane(i)
(checked as an index), Fixed.from([…]), shuffle(of: Fixed[u8, N]) with
constant indices (an index outside the lanes is NK1298), dot_pairs(w),
which multiplies lane by lane and adds each neighbouring pair into a lane of
twice the width (Fixed[u8, N] by Fixed[i8, N] gives Fixed[i16, N / 2],
Fixed[i16, N] by Fixed[i16, N] gives Fixed[i32, N / 2]), and
widen/narrow_saturating between lane widths.
+ - * are not lane operations.A lane’s + would either check every lane, which costs a compare and a
branch the vector code exists to avoid, or wrap, which + never does
elsewhere in Nikaia. The program names what it means: wrapping_add or
saturating_add. + - * on lanes is NK1296, whose help names both.
Mask[T] answers first() -> i64?, positions() -> Seq[i64] (rising),
count(), any(), all(), and combines with & | !. A bits() -> u64
would break on a target with more than 64 lanes (SVE). Each position is
at least 0 and below the lane count, and a proof may use it.
The prototype lowers to fixed-size arrays the backend vectorises, and to
core::arch intrinsics behind the CPU level where it does not; either way
no unsafe is written in the program. A Fixed that outruns the target’s
register is split.
simd::load(b, at), simd::splat(x), simd::Fixed.from([…]) and
simd::select(m, a, b) are functions of the module; the first three take
their type from what receives them, an annotated let or a parameter, and
without one they are NK1245.Fixed[T, N]’s mask is simd::Mask[T, N], a Lanes[T]’s simd::Mask[T];
v.count() is a vector’s lane count, the form D1’s Lanes[T].count takes.i8, u16 and i16 are lane types only, inside simd:: types.NK1296 covers / and % too; NK1299 refuses a lane shift whose count is
not a literal below the lane’s width.dot_pairs of u8 by i8 saturates into i16; of i16 by i16 it wraps
into i32, the rules of pmaddubsw and pmaddwd, which every target can
follow.0 <= p < 64 for the prover; a load is proved
for the most bytes any level reads (32).Settled by the supervising session from the prototype (#775).
LLVM forms a vector from the array lowering where whole vectors are loaded or
stored; lanes built from scalars and read back as scalars stay scalar code. In
1BRC a two-row temperature parse with Fixed[u64, 2] (beowolx’s technique)
cost 118.1 instructions a row at x86-64-v3 against 99.4 for the scalar
parse; the same parse written with core::arch intrinsics reached 97.8, so
even ideal lowering would save 1.6. The prototype therefore keeps the array
lowering and adds no unsafe crate for Fixed; one is decided when a program
measures a gain from it. A lane shift by a run-time count is not needed:
select between v >> 8 and v is one instruction.
From #775 step 6’s measurement.
Simd<T, N>): gives up the target’s width
for every loop that does not need a fixed one.+ on lanes: a compare and branch per operation.+ on lanes: + would mean two things.A shuffle by indices known only at run time (a table chosen by the input’s layout); Floating-point lanes; a store into a buffer; gathers and scatters; the CPU
level query and the first x86:: instructions (#755, questions 4 and 5);
crc32c and prefetch (questions 6 and 7). Whether the provisional status is
lifted is decided from the prototype’s measurement on 1BRC.