1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
//! The register-resident kernel: one shuffle per byte, no gather, no branch.
//!
//! A quotient has at most 16 blocks, so one transition row is 16 bytes — exactly
//! one SIMD register, and `shuffle(row, v)` applies that row to all 16 lanes at
//! once. That is Langdale's Sheng execution technique (*Say Hello To My Little
//! Friend*, 2018), applied here to an over-approximation rather than to an exact
//! small DFA. The row is a 16-byte load from a 4 KiB table that stays L1-resident,
//! so no step ever waits on a dependent load out of a full-size transition table.
//!
//! # What the register holds, and why that is the whole speed story
//!
//! The obvious thing to keep in the register is the **state**, broadcast to every
//! lane. It works, and it is what this kernel used to do — but it is a dead end,
//! because each shuffle needs the previous shuffle's answer. One byte per shuffle
//! *latency*, forever. Measured on an M4 that is 2 cycles a byte, and no amount of
//! unrolling moves it: there is one dependency chain and it is as long as the
//! document.
//!
//! So the register holds the **function** instead. Seed it with the identity
//! `[0,1,…,15]` and the same single shuffle per byte now composes rather than
//! steps: after any run of bytes, lane `i` holds *the block that run would reach if
//! it had started in block `i`* — all sixteen answers, for the price of the one.
//! Nothing in the loop depends on where the scan actually is, so the haystack
//! splits into `WAYS` slices that advance in lockstep with no dependency between
//! them, and the shuffle unit sees `WAYS` independent chains instead of one. The
//! per-byte instruction count is unchanged; only the critical path shrinks.
//!
//! Composition is closed under the same instruction, which is what makes the
//! bookkeeping free: `shuffle(g, f)` is `g ∘ f`, so collapsing the slices back into
//! one real trajectory at the end of a chunk costs a handful of shuffles per
//! `CHUNK` bytes.
//!
//! This is Mytkowicz, Musuvathi & Schulte's enumerative technique (*Data-parallel
//! finite-state machines*, ASPLOS 2014) — transitions from all states at once, as a
//! gather, implemented with a byte shuffle where no gather exists. Their overhead is
//! proportional to the state count, which is why they need convergence optimizations
//! to recover it; a sieve has already capped its machine at `LANES` blocks to fit
//! the register at all, so here enumeration is free. The bound that makes the filter
//! sound is the same one that makes it parallel.
//!
//! # The accept test is still exact, and still free
//!
//! Blocks were renumbered by the lattice harvest so every accepting block sorts
//! above every non-accepting one, which makes "did this ever accept?" an unsigned
//! **max** — one instruction per byte, off the critical path.
//!
//! Under function composition that max is per lane, so `high[i]` is the highest
//! block the slice would have visited *starting from `i`*. Resolving the chunk
//! walks the real state through the slices in order, reading each slice's max at
//! exactly the lane the real trajectory entered it on. The state entering slice `w`
//! is real by construction — it is the previous slices' composition applied to a
//! real state — so what comes out is the true maximum over the chunk, not a bound
//! on it. This kernel therefore refutes exactly the documents the scalar reference
//! refutes; the differential test in `tests/soundness.rs` holds it to that.
//!
//! Reading the whole 16-lane max instead would also be *sound* — it can only
//! over-report an accept, which costs a skip and never a wrong answer — but it
//! would be reading sixteen hypothetical scans as if they had all happened, and on
//! an unanchored pattern nearly every chunk would fail to refute. Selectivity is
//! the product here, so the kernel pays the few shuffles to keep it.
//!
//! Deleting the max altogether is possible and **measured not to be worth it**. Give
//! every accepting block a self-loop and "did it ever accept" collapses into the
//! final state, so the per-byte work drops from load+shuffle+max to load+shuffle.
//! That variant benchmarks at 0.131 ns/byte against this one's 0.134 — inside the
//! noise, because the loop is bound by the row load and the shuffle port, not by the
//! spare integer op riding beside them. It would have cost a second, trapping form
//! of every quotient (the selectivity chain needs the honest one, since an
//! absorbing accept drives its long-run rate to 1) for two percent. Left undone
//! deliberately.
use cratearch;
use crate;
use crateSkip;
/// Which byte-shuffle instruction set [`refutes`] dispatches to on the machine
/// that is running. See [`crate::arch::Kernel`] for what each variant means and
/// why it is reported rather than assumed.
pub use crateKernel;
/// What [`refutes`] will actually run here. See [`crate::arch::kernel`].
pub use cratekernel;
/// Bytes between accept checks. One resolution per chunk instead of per byte; the
/// per-lane max cannot lose an accept inside a chunk, so the only cost of a larger
/// chunk is overshooting past the first accepting position — and a refutation
/// filter does not care where the match was, only whether one is possible.
pub const CHUNK: usize = 256;
/// Independent composition chains advanced in lockstep.
///
/// This is the ILP dial, and it is set by the shuffle unit rather than by taste: a
/// chain issues one shuffle every `latency` cycles, so it takes about `latency`
/// chains to saturate a unit that retires one shuffle per cycle. Four covers the
/// 2-cycle `tbl` on Apple silicon and the 1-cycle `pshufb` on x86_64 with margin,
/// while keeping the working set — `WAYS` function registers plus `WAYS` max
/// registers plus the rows in flight — well inside sixteen architectural vector
/// registers, so nothing spills on the narrower of the two targets.
///
/// Swept on an M4 over 32 MiB of real source (`cargo run --release --example bench`,
/// geomean ns/byte): two 0.160, **four 0.134**, six 0.133, eight 0.140. Two is short
/// of covering the latency and eight starts costing more in register pressure and
/// short-document setup than the extra chains return, so the curve is flat exactly
/// where the latency argument says it should be.
pub const WAYS: usize = 4;
/// Bytes each chain walks per full chunk. The slices tile the chunk exactly, so the
/// four streams are four sequential reads and the prefetcher sees what it expects.
///
/// A short final chunk re-derives its own stride rather than falling back to the
/// scalar walk — a 64-byte document is entirely "final chunk", and handing that case
/// to the reference path made small documents several times *slower* than the kernel
/// they were supposed to be using.
pub const STRIDE: usize = CHUNK / WAYS;
/// The do-nothing function: lane `i` holds `i`, so shuffling by it is identity and
/// shuffling it by a row yields that row. Every chain starts here.
pub const IDENTITY: = ;
/// Does `q` **prove** `hay` holds no match?
///
/// `true` is a conclusive negative: the quotient recognizes a superset of the
/// pattern's language, so if the quotient never accepts, nothing does. `false`
/// means "cannot rule it out" and obliges the caller to run a real matcher — it
/// is never evidence of a match.
/// [`refutes`], but skipping the runs the quotient provably sits still through.
///
/// The loop is the classical accelerated-DFA shape: while the run is in the block
/// `skip` describes, ask [`Skip::find`] where the next byte that moves it is and
/// jump there; otherwise step. It is **exact, not approximate** — a self-loop byte
/// in a non-accepting block cannot change the state and cannot visit an accepting
/// block, so the bytes jumped over could not have contributed an answer.
///
/// Two preconditions, both established by [`Skip::of`] and the caller that stores
/// one: `skip.resident` must be non-accepting, and its escape set must be exactly
/// the bytes that leave it. A skip that overshoots would make the sieve reject a
/// document that matches, so neither is left to chance —
/// `tests/soundness.rs` runs this against [`scalar`] on every pattern that
/// harvests, and [`crate::Skip`] holds the instrument to its own definition.
///
/// Whether this beats [`refutes`] is an economic question, not a universal one:
/// the excursions between skips are walked one byte at a time, so a quotient that
/// leaves its resident block often pays more here than the four-way composition
/// costs. [`crate::price`] decides, and `examples/bench.rs` measures.
/// The reference semantics every vector path is checked against. Kept in the
/// shipping build, not behind `cfg(test)`: it is the fallback on any target
/// without a byte shuffle, and a differential test can only be honest if it
/// exercises the same code the fallback runs.
/// Step `q` across `hay` from `state`, or `None` the moment the run is known to
/// have visited an accepting block.
///
/// One chunk's worth of the scalar semantics, factored out because the vector
/// kernels in [`crate::arch`] need exactly it for the sub-chunk tail — a remainder
/// too short to slice is not worth a second code path to get wrong.
pub