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
//! Bit-parallel kernels with target-specific forms, and the dispatch that
//! picks one form per target.
//!
//! # Layout
//!
//! - [`portable`]: the reference form of every kernel below. Plain Rust,
//! correct on every target and the fallback wherever nothing else is
//! selected.
//! - `<target_arch>/<feature>`: forms that need an ISA extension, named
//! after the `target_feature` that defines the form (e.g.
//! [`x86_64::bmi1`], whose forms also need POPCNT; later `x86_64::avx2`,
//! `aarch64::neon`). Selected by runtime feature detection, or by
//! `cfg(target_feature)` where the target guarantees it.
//! - `<target_arch>/<uarch>`: forms that use only the baseline ISA but are
//! scheduled for one core family, named after that family as LLVM's
//! `target-cpu` names it ([`aarch64::apple`], [`aarch64::neoverse`]).
//! Selected at compile time by `cfg`, with `target_vendor = "apple"` as a
//! proxy: it picks by OS vendor, not by core. Apple targets always run
//! on Apple cores. Every other AArch64 target gets the Neoverse forms,
//! including Windows on Snapdragon Oryon, whose cores behave more like
//! Apple's (untuned: there is no runner to measure on).
//! - `<target_arch>`: forms that every core of one architecture runs, such
//! as the strips of [`aarch64::full`], which need AArch64's 31 registers.
//!
//! The split follows `memchr` (`arch/{generic, x86_64/avx2, aarch64/neon}`)
//! and RustCrypto's `sha2` (`soft`, `x86_sha`, `aarch64_sha2`), with module
//! names spelled like `std::arch` and `target_feature`. ISA features and
//! core tuning get separate names because only the former changes what a
//! CPU can run, as with `-march` and `-mtune` in GCC and Clang.
//!
//! # Naming
//!
//! A kernel has the same name and signature in every module that has a form
//! of it (`portable::multiword`, `aarch64::apple::multiword`,
//! `x86_64::bmi1::multiword`), so the path states the target. Code outside
//! this module calls only the dispatchers here ([`myers_step`],
//! [`multiword`], [`full`], [`lcs_full`]), which hold every selection rule.
//!
//! # Selection
//!
//! The dispatchers select with `#[cfg(...)]` on their statements, so a
//! build instantiates only the forms its target can select (with both
//! AArch64 forms of [`multiword`] instantiated in one function, LLVM
//! allocated the registers of the strips differently). [`full`] keeps
//! `if cfg!(...)`: with `#[cfg]`, the x86-64 code of the distance kernels
//! that call it changed size, and the shifted layout cost `ratio` 17% on
//! 800-character UCS-4 pairs on Linux x86-64. ISA features are
//! checked with `is_x86_feature_detected!` on every call, not through a
//! cached function pointer, which would keep [`multiword`] from being
//! inlined into the short-pattern path. std caches the result, so each
//! call costs one load and a branch.
//!
//! The crate itself is compiled for each target's baseline ISA; on x86-64
//! the hot path has a second copy with the x86-64-v2 features (see
//! [`crate::Isa`] and [`x86_64::v2`]). The dispatchers take the copy as
//! `X`: the forms of [`x86_64::bmi1`] need the x86-64-v2 features besides
//! BMI1, so only that copy selects them, and there the check for POPCNT
//! is `X::X86_64_V2`, a constant, as it was `cfg!(target_feature)` in the
//! crate-wide x86-64-v2 build before.
//!
//! # Measured and rejected
//!
//! - `x86_64::bmi1::multiword` from five words: the state spills out of
//! x86-64's 16 registers and the spills, not the ALU work, set the pace.
//! The and-not form saved under 2% of the instructions there and measured
//! within noise to 4% slower.
//! - An AVX2 `multiword` with the words as lanes of two vectors and the
//! carries rippled through lane masks: a column took 40-65% fewer
//! instructions but became one latency chain through the moves of the
//! masks between vector and scalar registers, 1.1 to 1.9 times slower at
//! five to eight words.
//! - [`full`] on x86-64 in strips of four words of `x86_64::bmi1::multiword`:
//! 12-23% slower than the portable loop over all words.
//! - [`full`] on x86-64 in strips of eight words of the AVX2 `multiword`: up
//! to 1.8 times slower than the portable loop.
//!
//! # Compilation and tests
//!
//! Every module is compiled on every target; only items that cannot be
//! (`asm!`, `#[target_feature]`, `std::arch` intrinsics) carry
//! `#[cfg(target_arch = ...)]`. `tests` checks each plain-Rust form
//! against the portable one or the DP oracle on every target, not only on
//! the one that selects it.
pub
use crate::;
/// Edge byte between stacked strips of a tall pattern (see
/// [`aarch64::full`]): bit 0 is the carry of the column addition,
/// bits 1 and 2 the `+1`/`-1` horizontal delta leaving the strip's top row.
/// Below row 0 the delta is `+1` and there is no carry.
const EDGE_ROW0: u8 = 0b010;
/// One column of the single-word Levenshtein recurrence (see
/// [`crate::hyrro_inner`]): returns the new `(vp, vn)`. Selects
/// [`aarch64::apple::myers_step`] on Apple targets,
/// [`aarch64::neoverse::myers_step`] on other AArch64 targets (by OS
/// vendor, not by core; see *Layout*), [`x86_64::myers_step`] on x86-64,
/// and [`portable::myers_step`] elsewhere.
pub
/// Multi-word Levenshtein kernel over `m` pattern rows in `W` words.
/// Returns the sum of the vertical deltas of these rows in the last text
/// column, i.e. their contribution to the distance on top of `D(0, n) = n`
/// (see [`crate::hyrro_inner`]). With `edges`, the kernel is one strip of
/// a taller pattern: it reads each column's incoming edge from there and
/// writes the outgoing one back.
///
/// Selects `x86_64::bmi1::multiword` for whole patterns (no `edges`) of up
/// to four words in the x86-64-v2 copy `X` on CPUs with BMI1; from five
/// words the state spills and the and-not form no longer pays (see
/// *Measured and rejected*). Otherwise selects like [`myers_step`]: the Apple form on
/// Apple targets, the Neoverse form on other AArch64 targets (by OS vendor,
/// not by core) and [`portable::multiword`] elsewhere.
pub
/// Full-matrix Levenshtein distance (fallback for `w > MAX_W`). Selects
/// the strips of [`aarch64::full`] on every AArch64 core, whose 31
/// general-purpose registers hold a strip's state, and the loop over all
/// words of [`portable::full`] elsewhere (strips lose on x86-64, see
/// *Measured and rejected*).
pub
/// Full-matrix LCS length (fallback for `w > MAX_W`). Selects strips in
/// the x86-64-v2 copy `X` on CPUs with BMI1 (`x86_64::bmi1::lcs_full`)
/// and on every AArch64 core ([`aarch64::lcs_full`]), and the loop over
/// all words of [`portable::lcs_full`] elsewhere.
pub
/// Sum of the vertical deltas `vp - vn` over the `m` pattern rows held in
/// `W` words; rows above the pattern in the top word hold garbage and are
/// masked off.