stride-align 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +201 -0
- data/NOTICE +255 -0
- data/README.md +107 -0
- data/data/bmpm_data/gen_approx_any.txt +131 -0
- data/data/bmpm_data/gen_approx_arabic.txt +26 -0
- data/data/bmpm_data/gen_approx_common.txt +233 -0
- data/data/bmpm_data/gen_approx_cyrillic.txt +18 -0
- data/data/bmpm_data/gen_approx_czech.txt +18 -0
- data/data/bmpm_data/gen_approx_dutch.txt +18 -0
- data/data/bmpm_data/gen_approx_english.txt +47 -0
- data/data/bmpm_data/gen_approx_french.txt +25 -0
- data/data/bmpm_data/gen_approx_german.txt +73 -0
- data/data/bmpm_data/gen_approx_greek.txt +18 -0
- data/data/bmpm_data/gen_approx_greeklatin.txt +20 -0
- data/data/bmpm_data/gen_approx_hebrew.txt +18 -0
- data/data/bmpm_data/gen_approx_hungarian.txt +18 -0
- data/data/bmpm_data/gen_approx_italian.txt +18 -0
- data/data/bmpm_data/gen_approx_polish.txt +84 -0
- data/data/bmpm_data/gen_approx_portuguese.txt +18 -0
- data/data/bmpm_data/gen_approx_romanian.txt +18 -0
- data/data/bmpm_data/gen_approx_russian.txt +48 -0
- data/data/bmpm_data/gen_approx_spanish.txt +21 -0
- data/data/bmpm_data/gen_approx_turkish.txt +18 -0
- data/data/bmpm_data/gen_exact_any.txt +40 -0
- data/data/bmpm_data/gen_exact_approx_common.txt +79 -0
- data/data/bmpm_data/gen_exact_arabic.txt +18 -0
- data/data/bmpm_data/gen_exact_common.txt +32 -0
- data/data/bmpm_data/gen_exact_cyrillic.txt +18 -0
- data/data/bmpm_data/gen_exact_czech.txt +18 -0
- data/data/bmpm_data/gen_exact_dutch.txt +18 -0
- data/data/bmpm_data/gen_exact_english.txt +18 -0
- data/data/bmpm_data/gen_exact_french.txt +18 -0
- data/data/bmpm_data/gen_exact_german.txt +18 -0
- data/data/bmpm_data/gen_exact_greek.txt +18 -0
- data/data/bmpm_data/gen_exact_greeklatin.txt +18 -0
- data/data/bmpm_data/gen_exact_hebrew.txt +18 -0
- data/data/bmpm_data/gen_exact_hungarian.txt +18 -0
- data/data/bmpm_data/gen_exact_italian.txt +18 -0
- data/data/bmpm_data/gen_exact_polish.txt +23 -0
- data/data/bmpm_data/gen_exact_portuguese.txt +18 -0
- data/data/bmpm_data/gen_exact_romanian.txt +18 -0
- data/data/bmpm_data/gen_exact_russian.txt +19 -0
- data/data/bmpm_data/gen_exact_spanish.txt +19 -0
- data/data/bmpm_data/gen_exact_turkish.txt +18 -0
- data/data/bmpm_data/gen_hebrew_common.txt +113 -0
- data/data/bmpm_data/gen_lang.txt +295 -0
- data/data/bmpm_data/gen_languages.txt +36 -0
- data/data/bmpm_data/gen_rules_any.txt +367 -0
- data/data/bmpm_data/gen_rules_arabic.txt +76 -0
- data/data/bmpm_data/gen_rules_cyrillic.txt +99 -0
- data/data/bmpm_data/gen_rules_czech.txt +67 -0
- data/data/bmpm_data/gen_rules_dutch.txt +78 -0
- data/data/bmpm_data/gen_rules_english.txt +113 -0
- data/data/bmpm_data/gen_rules_french.txt +114 -0
- data/data/bmpm_data/gen_rules_german.txt +129 -0
- data/data/bmpm_data/gen_rules_greek.txt +97 -0
- data/data/bmpm_data/gen_rules_greeklatin.txt +118 -0
- data/data/bmpm_data/gen_rules_hebrew.txt +62 -0
- data/data/bmpm_data/gen_rules_hungarian.txt +83 -0
- data/data/bmpm_data/gen_rules_italian.txt +77 -0
- data/data/bmpm_data/gen_rules_polish.txt +185 -0
- data/data/bmpm_data/gen_rules_portuguese.txt +105 -0
- data/data/bmpm_data/gen_rules_romanian.txt +64 -0
- data/data/bmpm_data/gen_rules_russian.txt +142 -0
- data/data/bmpm_data/gen_rules_spanish.txt +85 -0
- data/data/bmpm_data/gen_rules_turkish.txt +50 -0
- data/data/keyboard_data/qwerty.npy +0 -0
- data/data/matrix_data/BLOSUM100 +31 -0
- data/data/matrix_data/BLOSUM30 +31 -0
- data/data/matrix_data/BLOSUM35 +31 -0
- data/data/matrix_data/BLOSUM40 +31 -0
- data/data/matrix_data/BLOSUM45 +25 -0
- data/data/matrix_data/BLOSUM50 +25 -0
- data/data/matrix_data/BLOSUM55 +31 -0
- data/data/matrix_data/BLOSUM60 +31 -0
- data/data/matrix_data/BLOSUM62 +25 -0
- data/data/matrix_data/BLOSUM65 +31 -0
- data/data/matrix_data/BLOSUM70 +31 -0
- data/data/matrix_data/BLOSUM75 +31 -0
- data/data/matrix_data/BLOSUM80 +25 -0
- data/data/matrix_data/BLOSUM85 +31 -0
- data/data/matrix_data/BLOSUM90 +25 -0
- data/data/matrix_data/NUC.4.4 +25 -0
- data/data/matrix_data/PAM10 +34 -0
- data/data/matrix_data/PAM100 +34 -0
- data/data/matrix_data/PAM110 +34 -0
- data/data/matrix_data/PAM120 +34 -0
- data/data/matrix_data/PAM130 +34 -0
- data/data/matrix_data/PAM140 +34 -0
- data/data/matrix_data/PAM150 +34 -0
- data/data/matrix_data/PAM160 +34 -0
- data/data/matrix_data/PAM170 +34 -0
- data/data/matrix_data/PAM180 +34 -0
- data/data/matrix_data/PAM190 +34 -0
- data/data/matrix_data/PAM20 +34 -0
- data/data/matrix_data/PAM200 +34 -0
- data/data/matrix_data/PAM210 +34 -0
- data/data/matrix_data/PAM220 +34 -0
- data/data/matrix_data/PAM230 +34 -0
- data/data/matrix_data/PAM240 +34 -0
- data/data/matrix_data/PAM250 +25 -0
- data/data/matrix_data/PAM260 +34 -0
- data/data/matrix_data/PAM270 +34 -0
- data/data/matrix_data/PAM280 +34 -0
- data/data/matrix_data/PAM290 +34 -0
- data/data/matrix_data/PAM30 +25 -0
- data/data/matrix_data/PAM300 +34 -0
- data/data/matrix_data/PAM310 +34 -0
- data/data/matrix_data/PAM320 +34 -0
- data/data/matrix_data/PAM330 +34 -0
- data/data/matrix_data/PAM340 +34 -0
- data/data/matrix_data/PAM350 +34 -0
- data/data/matrix_data/PAM360 +34 -0
- data/data/matrix_data/PAM370 +34 -0
- data/data/matrix_data/PAM380 +34 -0
- data/data/matrix_data/PAM390 +34 -0
- data/data/matrix_data/PAM40 +34 -0
- data/data/matrix_data/PAM400 +34 -0
- data/data/matrix_data/PAM410 +34 -0
- data/data/matrix_data/PAM420 +34 -0
- data/data/matrix_data/PAM430 +34 -0
- data/data/matrix_data/PAM440 +34 -0
- data/data/matrix_data/PAM450 +34 -0
- data/data/matrix_data/PAM460 +34 -0
- data/data/matrix_data/PAM470 +34 -0
- data/data/matrix_data/PAM480 +34 -0
- data/data/matrix_data/PAM490 +34 -0
- data/data/matrix_data/PAM50 +34 -0
- data/data/matrix_data/PAM500 +34 -0
- data/data/matrix_data/PAM60 +34 -0
- data/data/matrix_data/PAM70 +25 -0
- data/data/matrix_data/PAM80 +34 -0
- data/data/matrix_data/PAM90 +34 -0
- data/ext/stride_align/backend_avx2.cpp +2 -0
- data/ext/stride_align/backend_avx512bwvl.cpp +2 -0
- data/ext/stride_align/backend_generic.cpp +3 -0
- data/ext/stride_align/backend_impl.hpp +983 -0
- data/ext/stride_align/backend_lasx.cpp +2 -0
- data/ext/stride_align/backend_lsx.cpp +2 -0
- data/ext/stride_align/backend_neon.cpp +2 -0
- data/ext/stride_align/backend_rvv.cpp +2 -0
- data/ext/stride_align/backend_sse41.cpp +2 -0
- data/ext/stride_align/backend_sve.cpp +2 -0
- data/ext/stride_align/backend_sve2.cpp +2 -0
- data/ext/stride_align/backend_vsx.cpp +2 -0
- data/ext/stride_align/beider_morse_impl.cpp +5 -0
- data/ext/stride_align/cpu_detect.cpp +172 -0
- data/ext/stride_align/cpu_detect.hpp +6 -0
- data/ext/stride_align/extconf.rb +114 -0
- data/ext/stride_align/target_profile.hpp +82 -0
- data/ext/stride_align/vendor/beider_morse_impl.cpp +1467 -0
- data/ext/stride_align/vendor/stride_align/alignment.hpp +199 -0
- data/ext/stride_align/vendor/stride_align/batch.hpp +812 -0
- data/ext/stride_align/vendor/stride_align/beider_morse.hpp +121 -0
- data/ext/stride_align/vendor/stride_align/caverphone.hpp +222 -0
- data/ext/stride_align/vendor/stride_align/cologne_phonetic.hpp +202 -0
- data/ext/stride_align/vendor/stride_align/core.hpp +731 -0
- data/ext/stride_align/vendor/stride_align/daitch_mokotoff.hpp +631 -0
- data/ext/stride_align/vendor/stride_align/double_metaphone.hpp +796 -0
- data/ext/stride_align/vendor/stride_align/dtw.hpp +300 -0
- data/ext/stride_align/vendor/stride_align/encoded.hpp +235 -0
- data/ext/stride_align/vendor/stride_align/hamming.hpp +55 -0
- data/ext/stride_align/vendor/stride_align/indel.hpp +1200 -0
- data/ext/stride_align/vendor/stride_align/jaro.hpp +517 -0
- data/ext/stride_align/vendor/stride_align/lcs.hpp +159 -0
- data/ext/stride_align/vendor/stride_align/levenshtein.hpp +1247 -0
- data/ext/stride_align/vendor/stride_align/levenshtein_prepared.hpp +193 -0
- data/ext/stride_align/vendor/stride_align/match_rating.hpp +168 -0
- data/ext/stride_align/vendor/stride_align/metaphone.hpp +291 -0
- data/ext/stride_align/vendor/stride_align/ngram.hpp +176 -0
- data/ext/stride_align/vendor/stride_align/nysiis.hpp +199 -0
- data/ext/stride_align/vendor/stride_align/pairwise_alignment.hpp +465 -0
- data/ext/stride_align/vendor/stride_align/partial_ratio.hpp +486 -0
- data/ext/stride_align/vendor/stride_align/ratcliff_obershelp.hpp +101 -0
- data/ext/stride_align/vendor/stride_align/soundex.hpp +108 -0
- data/ext/stride_align/vendor/stride_align/token_ratios.hpp +445 -0
- data/ext/stride_align/vendor/stride_align/types.hpp +16 -0
- data/ext/stride_align/vendor/stride_align/utf8.hpp +512 -0
- data/ext/stride_align/vendor/stride_align/wratio.hpp +363 -0
- data/lib/stride_align/algorithms.rb +296 -0
- data/lib/stride_align/alignment_path.rb +217 -0
- data/lib/stride_align/backend.rb +47 -0
- data/lib/stride_align/batch.rb +705 -0
- data/lib/stride_align/core.rb +180 -0
- data/lib/stride_align/keyboard.rb +200 -0
- data/lib/stride_align/matrices.rb +403 -0
- data/lib/stride_align/version.rb +5 -0
- data/lib/stride_align.rb +87 -0
- metadata +231 -0
|
@@ -0,0 +1,1200 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
// Indel distance — Levenshtein restricted to insertions and deletions
|
|
4
|
+
// (no substitutions). Equivalently:
|
|
5
|
+
//
|
|
6
|
+
// indel(a, b) = |a| + |b| - 2 * LCS(a, b)
|
|
7
|
+
//
|
|
8
|
+
// Normalized similarity:
|
|
9
|
+
//
|
|
10
|
+
// sim(a, b) = 1 - indel(a, b) / (|a| + |b|)
|
|
11
|
+
// = 2 * LCS(a, b) / (|a| + |b|)
|
|
12
|
+
//
|
|
13
|
+
// The bit-parallel kernel uses the Allison-Dix (1986) LCS recurrence:
|
|
14
|
+
// V starts as the all-ones m-bit vector.
|
|
15
|
+
// For each text character c:
|
|
16
|
+
// U = V & PEQ[c]
|
|
17
|
+
// V = ((V + U) | (V - U)) & MASK
|
|
18
|
+
// LCS = m - popcount(V).
|
|
19
|
+
//
|
|
20
|
+
// Hyyrö (2004) gives a cleaner derivation and the multi-word
|
|
21
|
+
// generalization; rapidfuzz uses the same recurrence.
|
|
22
|
+
|
|
23
|
+
#include <algorithm>
|
|
24
|
+
#include <array>
|
|
25
|
+
#include <bit>
|
|
26
|
+
#include <cstddef>
|
|
27
|
+
#include <cstdint>
|
|
28
|
+
#include <limits>
|
|
29
|
+
#include <span>
|
|
30
|
+
#include <type_traits>
|
|
31
|
+
#include <unordered_map>
|
|
32
|
+
#include <vector>
|
|
33
|
+
|
|
34
|
+
#include "stride_align/alignment.hpp"
|
|
35
|
+
|
|
36
|
+
namespace stride_align::indel {
|
|
37
|
+
|
|
38
|
+
inline constexpr std::size_t kNoCutoff = std::numeric_limits<std::size_t>::max();
|
|
39
|
+
|
|
40
|
+
// Scalar DP reference. O(m*n) time, O(m) space (rolling rows).
|
|
41
|
+
// Correctness oracle for the bit-parallel paths.
|
|
42
|
+
template <typename Token>
|
|
43
|
+
inline std::size_t indel_dp(
|
|
44
|
+
std::span<const Token> pattern,
|
|
45
|
+
std::span<const Token> text) {
|
|
46
|
+
const std::size_t m = pattern.size();
|
|
47
|
+
const std::size_t n = text.size();
|
|
48
|
+
if (m == 0U) return n;
|
|
49
|
+
if (n == 0U) return m;
|
|
50
|
+
|
|
51
|
+
std::vector<std::size_t> prev(m + 1U);
|
|
52
|
+
std::vector<std::size_t> curr(m + 1U);
|
|
53
|
+
for (std::size_t i = 0; i <= m; ++i) {
|
|
54
|
+
prev[i] = i;
|
|
55
|
+
}
|
|
56
|
+
for (std::size_t j = 1; j <= n; ++j) {
|
|
57
|
+
curr[0] = j;
|
|
58
|
+
for (std::size_t i = 1; i <= m; ++i) {
|
|
59
|
+
if (pattern[i - 1U] == text[j - 1U]) {
|
|
60
|
+
curr[i] = prev[i - 1U];
|
|
61
|
+
} else {
|
|
62
|
+
// No substitution: only insertion or deletion.
|
|
63
|
+
curr[i] = std::min(prev[i] + 1U, curr[i - 1U] + 1U);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
std::swap(prev, curr);
|
|
67
|
+
}
|
|
68
|
+
return prev[m];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Bit-parallel single-word indel for uint8 patterns of length <= 64.
|
|
72
|
+
//
|
|
73
|
+
// ``cutoff`` is the rapidfuzz-style score-cutoff: if the caller knows
|
|
74
|
+
// the result will only be used when distance <= cutoff, the kernel
|
|
75
|
+
// bails out of the per-character loop when its lower-bound estimate
|
|
76
|
+
// of the final distance exceeds the cutoff, returning ``cutoff + 1``
|
|
77
|
+
// (any value strictly greater than the cutoff carries the same
|
|
78
|
+
// "doesn't qualify" signal). The lower-bound formula: after
|
|
79
|
+
// processing ``j`` of ``n`` text chars with ``lcs_so_far = m -
|
|
80
|
+
// popcount(V)``, the final LCS can grow by at most ``n - j`` (each
|
|
81
|
+
// remaining text char can match at most one unused pattern slot), so
|
|
82
|
+
// the lower bound on final indel is
|
|
83
|
+
// m + n - 2·(lcs_so_far + (n - j)) = m - n + 2j - 2·lcs_so_far.
|
|
84
|
+
// Equivalently the bail condition simplifies to
|
|
85
|
+
// ``2*(j + popcount(V)) < m + n - cutoff``.
|
|
86
|
+
inline std::size_t indel_single_word_u8(
|
|
87
|
+
std::span<const std::uint8_t> pattern,
|
|
88
|
+
std::span<const std::uint8_t> text,
|
|
89
|
+
std::size_t cutoff = kNoCutoff) noexcept {
|
|
90
|
+
const std::size_t m = pattern.size();
|
|
91
|
+
const std::size_t n = text.size();
|
|
92
|
+
if (m == 0U) return n;
|
|
93
|
+
if (n == 0U) return m;
|
|
94
|
+
// indel = m + n - 2·LCS ≥ |m - n|. Bail before PEQ build when a
|
|
95
|
+
// finite cutoff already rules the pair out.
|
|
96
|
+
if (cutoff != kNoCutoff) {
|
|
97
|
+
const std::size_t len_diff = m > n ? m - n : n - m;
|
|
98
|
+
if (len_diff > cutoff) return cutoff + 1U;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
std::uint64_t peq[256] = {0};
|
|
102
|
+
const std::uint64_t one = 1U;
|
|
103
|
+
for (std::size_t i = 0; i < m; ++i) {
|
|
104
|
+
peq[pattern[i]] |= one << i;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const std::uint64_t mask =
|
|
108
|
+
(m == 64U) ? ~std::uint64_t{0} : ((one << m) - 1U);
|
|
109
|
+
std::uint64_t V = mask;
|
|
110
|
+
|
|
111
|
+
// Dual loops: the common no-cutoff path skips per-char popcount.
|
|
112
|
+
if (cutoff == kNoCutoff) {
|
|
113
|
+
for (const std::uint8_t c : text) {
|
|
114
|
+
const std::uint64_t U = V & peq[c];
|
|
115
|
+
V = ((V + U) | (V - U)) & mask;
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
// Pre-compute the bail threshold once. The lower bound on final
|
|
119
|
+
// indel after processing ``j`` of ``n`` text chars is
|
|
120
|
+
// 2*(j + popcount(V)) - m - n
|
|
121
|
+
// (derivation: final LCS ≤ (m - popcount(V)) + (n - j), so final
|
|
122
|
+
// indel = m + n - 2·final_lcs ≥ 2j + 2·popcount(V) - m - n).
|
|
123
|
+
// Bail when this lower bound exceeds the cutoff, i.e. when
|
|
124
|
+
// 2*(j + popcount(V)) > m + n + cutoff.
|
|
125
|
+
const std::size_t bail_threshold = m + n + cutoff;
|
|
126
|
+
std::size_t j = 0;
|
|
127
|
+
for (const std::uint8_t c : text) {
|
|
128
|
+
const std::uint64_t U = V & peq[c];
|
|
129
|
+
V = ((V + U) | (V - U)) & mask;
|
|
130
|
+
++j;
|
|
131
|
+
const std::size_t lower_bound_score =
|
|
132
|
+
2U * (j + static_cast<std::size_t>(std::popcount(V)));
|
|
133
|
+
if (lower_bound_score > bail_threshold) {
|
|
134
|
+
return cutoff + 1U;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const std::size_t lcs =
|
|
139
|
+
m - static_cast<std::size_t>(std::popcount(V));
|
|
140
|
+
return m + n - 2U * lcs;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Forward declarations: the dispatch helpers above and the
|
|
144
|
+
// templated ``indel_distance`` below need these visible before the
|
|
145
|
+
// concrete definitions further down in the file.
|
|
146
|
+
inline std::size_t indel_distance_k2_u8(
|
|
147
|
+
std::span<const std::uint8_t> pattern,
|
|
148
|
+
std::span<const std::uint8_t> text,
|
|
149
|
+
std::size_t cutoff = kNoCutoff) noexcept;
|
|
150
|
+
inline std::size_t indel_distance_k3_u8(
|
|
151
|
+
std::span<const std::uint8_t> pattern,
|
|
152
|
+
std::span<const std::uint8_t> text,
|
|
153
|
+
std::size_t cutoff = kNoCutoff) noexcept;
|
|
154
|
+
inline std::size_t indel_distance_k4_u8(
|
|
155
|
+
std::span<const std::uint8_t> pattern,
|
|
156
|
+
std::span<const std::uint8_t> text,
|
|
157
|
+
std::size_t cutoff = kNoCutoff) noexcept;
|
|
158
|
+
inline std::size_t indel_distance_k5_u8(
|
|
159
|
+
std::span<const std::uint8_t> pattern,
|
|
160
|
+
std::span<const std::uint8_t> text,
|
|
161
|
+
std::size_t cutoff = kNoCutoff) noexcept;
|
|
162
|
+
inline std::size_t indel_distance_k6_u8(
|
|
163
|
+
std::span<const std::uint8_t> pattern,
|
|
164
|
+
std::span<const std::uint8_t> text,
|
|
165
|
+
std::size_t cutoff = kNoCutoff) noexcept;
|
|
166
|
+
inline std::size_t indel_distance_k7_u8(
|
|
167
|
+
std::span<const std::uint8_t> pattern,
|
|
168
|
+
std::span<const std::uint8_t> text,
|
|
169
|
+
std::size_t cutoff = kNoCutoff) noexcept;
|
|
170
|
+
inline std::size_t indel_distance_k8_u8(
|
|
171
|
+
std::span<const std::uint8_t> pattern,
|
|
172
|
+
std::span<const std::uint8_t> text,
|
|
173
|
+
std::size_t cutoff = kNoCutoff) noexcept;
|
|
174
|
+
|
|
175
|
+
inline std::size_t indel_distance_multi_word_u8(
|
|
176
|
+
std::span<const std::uint8_t> pattern,
|
|
177
|
+
std::span<const std::uint8_t> text,
|
|
178
|
+
std::size_t cutoff = kNoCutoff);
|
|
179
|
+
|
|
180
|
+
template <typename Token>
|
|
181
|
+
inline std::size_t indel_distance_multi_word(
|
|
182
|
+
std::span<const Token> pattern,
|
|
183
|
+
std::span<const Token> text,
|
|
184
|
+
std::size_t cutoff = kNoCutoff);
|
|
185
|
+
|
|
186
|
+
// Token-generic bit-parallel for patterns of any length. Patterns of
|
|
187
|
+
// up to 64 elements use the cheap single-word path; longer patterns
|
|
188
|
+
// fall through to the multi-word Hyyrö generalisation. Same cutoff
|
|
189
|
+
// semantics as ``indel_single_word_u8``.
|
|
190
|
+
template <typename Token>
|
|
191
|
+
inline std::size_t indel_distance(
|
|
192
|
+
std::span<const Token> pattern,
|
|
193
|
+
std::span<const Token> text,
|
|
194
|
+
std::size_t cutoff = kNoCutoff) {
|
|
195
|
+
static_assert(std::is_integral_v<Token> || std::is_unsigned_v<Token>);
|
|
196
|
+
const std::size_t m = pattern.size();
|
|
197
|
+
const std::size_t n = text.size();
|
|
198
|
+
if (m == 0U) return n;
|
|
199
|
+
if (n == 0U) return m;
|
|
200
|
+
if (cutoff != kNoCutoff) {
|
|
201
|
+
const std::size_t len_diff = m > n ? m - n : n - m;
|
|
202
|
+
if (len_diff > cutoff) return cutoff + 1U;
|
|
203
|
+
}
|
|
204
|
+
if (m > 64U) {
|
|
205
|
+
return indel_distance_multi_word<Token>(pattern, text, cutoff);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
std::unordered_map<Token, std::uint64_t> peq;
|
|
209
|
+
const std::uint64_t one = 1U;
|
|
210
|
+
for (std::size_t i = 0; i < m; ++i) {
|
|
211
|
+
peq[pattern[i]] |= one << i;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const std::uint64_t mask =
|
|
215
|
+
(m == 64U) ? ~std::uint64_t{0} : ((one << m) - 1U);
|
|
216
|
+
std::uint64_t V = mask;
|
|
217
|
+
|
|
218
|
+
if (cutoff == kNoCutoff) {
|
|
219
|
+
for (const auto c : text) {
|
|
220
|
+
auto it = peq.find(c);
|
|
221
|
+
const std::uint64_t pm = (it == peq.end()) ? 0U : it->second;
|
|
222
|
+
const std::uint64_t U = V & pm;
|
|
223
|
+
V = ((V + U) | (V - U)) & mask;
|
|
224
|
+
}
|
|
225
|
+
} else {
|
|
226
|
+
const std::size_t bail_threshold = m + n + cutoff;
|
|
227
|
+
std::size_t j = 0;
|
|
228
|
+
for (const auto c : text) {
|
|
229
|
+
auto it = peq.find(c);
|
|
230
|
+
const std::uint64_t pm = (it == peq.end()) ? 0U : it->second;
|
|
231
|
+
const std::uint64_t U = V & pm;
|
|
232
|
+
V = ((V + U) | (V - U)) & mask;
|
|
233
|
+
++j;
|
|
234
|
+
const std::size_t lower_bound_score =
|
|
235
|
+
2U * (j + static_cast<std::size_t>(std::popcount(V)));
|
|
236
|
+
if (lower_bound_score > bail_threshold) {
|
|
237
|
+
return cutoff + 1U;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const std::size_t lcs =
|
|
242
|
+
m - static_cast<std::size_t>(std::popcount(V));
|
|
243
|
+
return m + n - 2U * lcs;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Convenience dispatch for u8 patterns: single-word bit-parallel for
|
|
247
|
+
// short patterns, scalar DP for longer. Cutoff is honoured by the
|
|
248
|
+
// bit-parallel path; the scalar DP path doesn't implement early-
|
|
249
|
+
// exit yet, so a cutoff there is informational only (the full
|
|
250
|
+
// distance is computed and returned).
|
|
251
|
+
inline std::size_t indel_distance_u8(
|
|
252
|
+
std::span<const std::uint8_t> pattern,
|
|
253
|
+
std::span<const std::uint8_t> text,
|
|
254
|
+
std::size_t cutoff = kNoCutoff) {
|
|
255
|
+
const std::size_t m = pattern.size();
|
|
256
|
+
const std::size_t n = text.size();
|
|
257
|
+
if (m == 0U) return n;
|
|
258
|
+
if (n == 0U) return m;
|
|
259
|
+
if (cutoff != kNoCutoff) {
|
|
260
|
+
const std::size_t len_diff = m > n ? m - n : n - m;
|
|
261
|
+
if (len_diff > cutoff) return cutoff + 1U;
|
|
262
|
+
}
|
|
263
|
+
if (m > 0U && m <= 64U) {
|
|
264
|
+
return indel_single_word_u8(pattern, text, cutoff);
|
|
265
|
+
}
|
|
266
|
+
if (m > 64U && m <= 128U) {
|
|
267
|
+
return indel_distance_k2_u8(pattern, text, cutoff);
|
|
268
|
+
}
|
|
269
|
+
if (m > 128U && m <= 192U) {
|
|
270
|
+
return indel_distance_k3_u8(pattern, text, cutoff);
|
|
271
|
+
}
|
|
272
|
+
if (m > 192U && m <= 256U) {
|
|
273
|
+
return indel_distance_k4_u8(pattern, text, cutoff);
|
|
274
|
+
}
|
|
275
|
+
if (m > 256U && m <= 320U) {
|
|
276
|
+
return indel_distance_k5_u8(pattern, text, cutoff);
|
|
277
|
+
}
|
|
278
|
+
if (m > 320U && m <= 384U) {
|
|
279
|
+
return indel_distance_k6_u8(pattern, text, cutoff);
|
|
280
|
+
}
|
|
281
|
+
if (m > 384U && m <= 448U) {
|
|
282
|
+
return indel_distance_k7_u8(pattern, text, cutoff);
|
|
283
|
+
}
|
|
284
|
+
if (m > 448U && m <= 512U) {
|
|
285
|
+
return indel_distance_k8_u8(pattern, text, cutoff);
|
|
286
|
+
}
|
|
287
|
+
return indel_distance_multi_word_u8(pattern, text, cutoff);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Byte-alphabet multi-word Indel. Specialised for ``std::uint8_t``
|
|
291
|
+
// (and the codepoint-fits-in-byte fast path) — the 256-entry PEQ
|
|
292
|
+
// lives in a single contiguous allocation instead of the per-unique-
|
|
293
|
+
// char ``std::vector`` allocations that the generic ``unordered_map``
|
|
294
|
+
// path needs. For long ASCII / Latin-1 inputs this is the difference
|
|
295
|
+
// between a single 16 KB malloc and ~30 separate small allocations,
|
|
296
|
+
// which dominate the per-call cost for long patterns.
|
|
297
|
+
// Per-thread scratch buffers shared across the multi-word u8 kernel.
|
|
298
|
+
// The kernel only ever resizes the buffers upward, so reuse across
|
|
299
|
+
// calls amortises away the heap-allocation cost that was the
|
|
300
|
+
// dominant per-call expense for long patterns.
|
|
301
|
+
//
|
|
302
|
+
// PEQ clearing uses a dirty-symbol list: after the first call we only
|
|
303
|
+
// zero the alphabet rows the previous pattern touched (typically ≪ 256
|
|
304
|
+
// for ASCII), instead of ``fill(256 * K)`` every time.
|
|
305
|
+
struct MultiWordU8Scratch {
|
|
306
|
+
std::vector<std::uint64_t> peq; // 256 * K entries
|
|
307
|
+
std::vector<std::uint64_t> mask; // K entries
|
|
308
|
+
std::vector<std::uint64_t> V; // K entries
|
|
309
|
+
std::vector<std::uint64_t> sum; // K entries
|
|
310
|
+
std::vector<std::uint64_t> diff; // K entries
|
|
311
|
+
std::vector<std::uint64_t> U; // K entries
|
|
312
|
+
|
|
313
|
+
// Dirty-PEQ bookkeeping.
|
|
314
|
+
std::vector<std::uint8_t> peq_dirty;
|
|
315
|
+
std::array<std::uint32_t, 256> peq_touch{};
|
|
316
|
+
std::uint32_t peq_gen = 0;
|
|
317
|
+
std::size_t peq_layout_k = 0;
|
|
318
|
+
|
|
319
|
+
void resize_for(std::size_t K) {
|
|
320
|
+
if (peq.size() < 256U * K) {
|
|
321
|
+
peq.assign(256U * K, 0U);
|
|
322
|
+
peq_dirty.clear();
|
|
323
|
+
peq_layout_k = K;
|
|
324
|
+
peq_touch.fill(0);
|
|
325
|
+
peq_gen = 0;
|
|
326
|
+
}
|
|
327
|
+
if (mask.size() < K) mask.resize(K);
|
|
328
|
+
if (V.size() < K) V.resize(K);
|
|
329
|
+
if (sum.size() < K) sum.resize(K);
|
|
330
|
+
if (diff.size() < K) diff.resize(K);
|
|
331
|
+
if (U.size() < K) U.resize(K);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Prepare peq for a rebuild at layout ``K``. Zeros only previously
|
|
335
|
+
// dirtied symbol rows when the layout is unchanged.
|
|
336
|
+
void peq_begin(std::size_t K) {
|
|
337
|
+
resize_for(K);
|
|
338
|
+
if (peq_layout_k != K) {
|
|
339
|
+
std::fill_n(peq.data(), 256U * K, std::uint64_t{0});
|
|
340
|
+
peq_dirty.clear();
|
|
341
|
+
peq_layout_k = K;
|
|
342
|
+
} else {
|
|
343
|
+
for (const std::uint8_t s : peq_dirty) {
|
|
344
|
+
std::fill_n(
|
|
345
|
+
peq.data() + static_cast<std::size_t>(s) * K, K, std::uint64_t{0});
|
|
346
|
+
}
|
|
347
|
+
peq_dirty.clear();
|
|
348
|
+
}
|
|
349
|
+
if (++peq_gen == 0U) {
|
|
350
|
+
peq_touch.fill(0);
|
|
351
|
+
peq_gen = 1U;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
void peq_or_bit(std::uint8_t c, std::size_t bit_index, std::size_t K) {
|
|
356
|
+
if (peq_touch[c] != peq_gen) {
|
|
357
|
+
peq_touch[c] = peq_gen;
|
|
358
|
+
peq_dirty.push_back(c);
|
|
359
|
+
}
|
|
360
|
+
peq[static_cast<std::size_t>(c) * K + (bit_index >> 6U)] |=
|
|
361
|
+
std::uint64_t{1} << (bit_index & 63U);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
void peq_build(std::span<const std::uint8_t> pattern, std::size_t K) {
|
|
365
|
+
peq_begin(K);
|
|
366
|
+
for (std::size_t i = 0; i < pattern.size(); ++i) {
|
|
367
|
+
peq_or_bit(pattern[i], i, K);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
inline MultiWordU8Scratch& multi_word_u8_scratch() {
|
|
373
|
+
thread_local MultiWordU8Scratch s;
|
|
374
|
+
return s;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
inline std::size_t indel_distance_multi_word_u8(
|
|
378
|
+
std::span<const std::uint8_t> pattern,
|
|
379
|
+
std::span<const std::uint8_t> text,
|
|
380
|
+
std::size_t cutoff) {
|
|
381
|
+
const std::size_t m = pattern.size();
|
|
382
|
+
const std::size_t n = text.size();
|
|
383
|
+
if (m == 0U) return n;
|
|
384
|
+
if (n == 0U) return m;
|
|
385
|
+
if (cutoff != kNoCutoff) {
|
|
386
|
+
const std::size_t len_diff = m > n ? m - n : n - m;
|
|
387
|
+
if (len_diff > cutoff) return cutoff + 1U;
|
|
388
|
+
}
|
|
389
|
+
const std::size_t K = (m + 63U) / 64U;
|
|
390
|
+
const std::uint64_t one = 1U;
|
|
391
|
+
|
|
392
|
+
MultiWordU8Scratch& scr = multi_word_u8_scratch();
|
|
393
|
+
scr.peq_build(pattern, K);
|
|
394
|
+
|
|
395
|
+
std::uint64_t* const mask = scr.mask.data();
|
|
396
|
+
for (std::size_t k = 0; k < K; ++k) mask[k] = ~std::uint64_t{0};
|
|
397
|
+
if (m % 64U != 0U) {
|
|
398
|
+
mask[K - 1] = (one << (m % 64U)) - 1U;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
std::uint64_t* const V = scr.V.data();
|
|
402
|
+
for (std::size_t k = 0; k < K; ++k) V[k] = mask[k];
|
|
403
|
+
|
|
404
|
+
const bool has_cutoff = cutoff != kNoCutoff;
|
|
405
|
+
const std::size_t bail_threshold = m + n + (has_cutoff ? cutoff : 0U);
|
|
406
|
+
|
|
407
|
+
std::size_t j = 0;
|
|
408
|
+
for (const std::uint8_t c : text) {
|
|
409
|
+
const std::uint64_t* peq_row = scr.peq.data() + static_cast<std::size_t>(c) * K;
|
|
410
|
+
|
|
411
|
+
// Fused per-block recurrence. The Hyyrö invariant ``U <= V``
|
|
412
|
+
// bitwise means the per-block subtraction ``V - U`` never
|
|
413
|
+
// underflows, so the borrow chain across blocks is identically
|
|
414
|
+
// zero. Only the carry chain on the addition needs to
|
|
415
|
+
// propagate — handled by ``__builtin_add_overflow``.
|
|
416
|
+
std::uint64_t carry_in = 0;
|
|
417
|
+
for (std::size_t k = 0; k < K; ++k) {
|
|
418
|
+
const std::uint64_t Vk = V[k];
|
|
419
|
+
const std::uint64_t Uk = Vk & peq_row[k];
|
|
420
|
+
std::uint64_t s1;
|
|
421
|
+
const bool c1 = __builtin_add_overflow(Vk, Uk, &s1);
|
|
422
|
+
std::uint64_t s2;
|
|
423
|
+
const bool c2 = __builtin_add_overflow(s1, carry_in, &s2);
|
|
424
|
+
carry_in = static_cast<std::uint64_t>(c1) + static_cast<std::uint64_t>(c2);
|
|
425
|
+
V[k] = (s2 | (Vk - Uk)) & mask[k];
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
++j;
|
|
429
|
+
if (has_cutoff) {
|
|
430
|
+
std::size_t pc = 0;
|
|
431
|
+
for (std::size_t k = 0; k < K; ++k) {
|
|
432
|
+
pc += static_cast<std::size_t>(std::popcount(V[k]));
|
|
433
|
+
}
|
|
434
|
+
const std::size_t lower_bound_score = 2U * (j + pc);
|
|
435
|
+
if (lower_bound_score > bail_threshold) {
|
|
436
|
+
return cutoff + 1U;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
std::size_t lcs_unmatched = 0;
|
|
442
|
+
for (std::size_t k = 0; k < K; ++k) {
|
|
443
|
+
lcs_unmatched += static_cast<std::size_t>(std::popcount(V[k]));
|
|
444
|
+
}
|
|
445
|
+
const std::size_t lcs = m - lcs_unmatched;
|
|
446
|
+
return m + n - 2U * lcs;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Hand-specialised K = 2 multi-word kernel for patterns 65..128
|
|
450
|
+
// characters. Architectural inspiration: rapidfuzz-cpp's
|
|
451
|
+
// ``lcs_unroll`` template specialisations (MIT, attribution in
|
|
452
|
+
// NOTICE). The two ideas inherited from their design are:
|
|
453
|
+
//
|
|
454
|
+
// 1. Per-block fused single-step Hyyrö recurrence —
|
|
455
|
+
// U[k] = V[k] & PEQ[c][k]
|
|
456
|
+
// sum = V[k] + U[k] + carry_in (carry propagates k → k+1)
|
|
457
|
+
// V[k] = (sum | (V[k] - U[k])) & mask[k]
|
|
458
|
+
// with NO separate borrow chain across blocks. The Hyyrö
|
|
459
|
+
// invariant ``U[k] <= V[k]`` bitwise guarantees that the
|
|
460
|
+
// per-block subtraction never underflows, so the cross-block
|
|
461
|
+
// borrow term is mathematically zero. Eliminates one of the
|
|
462
|
+
// four K-loops a naïve translation of Allison-Dix would emit.
|
|
463
|
+
// 2. State register-pinning. ``V0`` and ``V1`` are local
|
|
464
|
+
// ``uint64_t`` (no array, no intermediate buffers), so the
|
|
465
|
+
// compiler keeps them in registers across the entire text scan.
|
|
466
|
+
//
|
|
467
|
+
// Carry between block 0 and block 1 of the sum chain still needs
|
|
468
|
+
// propagation — handled with ``__builtin_add_overflow``, which the
|
|
469
|
+
// compiler lowers to ADCX/ADOX or equivalent.
|
|
470
|
+
//
|
|
471
|
+
// Correctness vs the generic multi-word kernel is verified on
|
|
472
|
+
// random fuzz (see tests/test_indel.py); the mathematical
|
|
473
|
+
// equivalence reduces to "the borrow chain is identically zero".
|
|
474
|
+
inline std::size_t indel_distance_k2_u8(
|
|
475
|
+
std::span<const std::uint8_t> pattern,
|
|
476
|
+
std::span<const std::uint8_t> text,
|
|
477
|
+
std::size_t cutoff) noexcept {
|
|
478
|
+
const std::size_t m = pattern.size();
|
|
479
|
+
const std::size_t n = text.size();
|
|
480
|
+
if (m == 0U) return n;
|
|
481
|
+
if (n == 0U) return m;
|
|
482
|
+
// Caller guarantees 64 < m <= 128.
|
|
483
|
+
constexpr std::size_t K = 2;
|
|
484
|
+
const std::uint64_t one = 1U;
|
|
485
|
+
|
|
486
|
+
MultiWordU8Scratch& scr = multi_word_u8_scratch();
|
|
487
|
+
scr.peq_build(pattern, K);
|
|
488
|
+
|
|
489
|
+
const std::uint64_t mask0 = ~std::uint64_t{0};
|
|
490
|
+
const std::uint64_t mask1 =
|
|
491
|
+
(m % 64U == 0U) ? ~std::uint64_t{0} : ((one << (m % 64U)) - 1U);
|
|
492
|
+
|
|
493
|
+
std::uint64_t V0 = mask0;
|
|
494
|
+
std::uint64_t V1 = mask1;
|
|
495
|
+
|
|
496
|
+
const std::uint64_t* const peq_base = scr.peq.data();
|
|
497
|
+
|
|
498
|
+
const bool has_cutoff = cutoff != kNoCutoff;
|
|
499
|
+
const std::size_t bail_threshold = m + n + (has_cutoff ? cutoff : 0U);
|
|
500
|
+
|
|
501
|
+
std::size_t j = 0;
|
|
502
|
+
for (const std::uint8_t c : text) {
|
|
503
|
+
const std::uint64_t* peq_row = peq_base + static_cast<std::size_t>(c) * K;
|
|
504
|
+
|
|
505
|
+
// Block 0: no carry in. The per-block fused expression.
|
|
506
|
+
const std::uint64_t U0 = V0 & peq_row[0];
|
|
507
|
+
std::uint64_t sum0;
|
|
508
|
+
const bool carry01 = __builtin_add_overflow(V0, U0, &sum0);
|
|
509
|
+
V0 = (sum0 | (V0 - U0)) & mask0;
|
|
510
|
+
|
|
511
|
+
// Block 1: carry in from block 0; no carry out needed (K = 2).
|
|
512
|
+
const std::uint64_t U1 = V1 & peq_row[1];
|
|
513
|
+
std::uint64_t sum1;
|
|
514
|
+
__builtin_add_overflow(V1, U1, &sum1);
|
|
515
|
+
sum1 += static_cast<std::uint64_t>(carry01);
|
|
516
|
+
V1 = (sum1 | (V1 - U1)) & mask1;
|
|
517
|
+
|
|
518
|
+
++j;
|
|
519
|
+
if (has_cutoff) {
|
|
520
|
+
const std::size_t pc =
|
|
521
|
+
static_cast<std::size_t>(std::popcount(V0)) +
|
|
522
|
+
static_cast<std::size_t>(std::popcount(V1));
|
|
523
|
+
const std::size_t lower_bound_score = 2U * (j + pc);
|
|
524
|
+
if (lower_bound_score > bail_threshold) {
|
|
525
|
+
return cutoff + 1U;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const std::size_t lcs_unmatched =
|
|
531
|
+
static_cast<std::size_t>(std::popcount(V0)) +
|
|
532
|
+
static_cast<std::size_t>(std::popcount(V1));
|
|
533
|
+
const std::size_t lcs = m - lcs_unmatched;
|
|
534
|
+
return m + n - 2U * lcs;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// K = 3 hand-specialisation for ``129 <= m <= 192``. Same fused
|
|
538
|
+
// single-step recurrence as K=2, extended to three register-pinned
|
|
539
|
+
// blocks with a two-stage carry chain.
|
|
540
|
+
inline std::size_t indel_distance_k3_u8(
|
|
541
|
+
std::span<const std::uint8_t> pattern,
|
|
542
|
+
std::span<const std::uint8_t> text,
|
|
543
|
+
std::size_t cutoff) noexcept {
|
|
544
|
+
const std::size_t m = pattern.size();
|
|
545
|
+
const std::size_t n = text.size();
|
|
546
|
+
if (m == 0U) return n;
|
|
547
|
+
if (n == 0U) return m;
|
|
548
|
+
// Caller guarantees 128 < m <= 192.
|
|
549
|
+
constexpr std::size_t K = 3;
|
|
550
|
+
const std::uint64_t one = 1U;
|
|
551
|
+
|
|
552
|
+
MultiWordU8Scratch& scr = multi_word_u8_scratch();
|
|
553
|
+
scr.peq_build(pattern, K);
|
|
554
|
+
|
|
555
|
+
const std::uint64_t mask0 = ~std::uint64_t{0};
|
|
556
|
+
const std::uint64_t mask1 = ~std::uint64_t{0};
|
|
557
|
+
const std::uint64_t mask2 =
|
|
558
|
+
(m % 64U == 0U) ? ~std::uint64_t{0} : ((one << (m % 64U)) - 1U);
|
|
559
|
+
|
|
560
|
+
std::uint64_t V0 = mask0;
|
|
561
|
+
std::uint64_t V1 = mask1;
|
|
562
|
+
std::uint64_t V2 = mask2;
|
|
563
|
+
|
|
564
|
+
const std::uint64_t* const peq_base = scr.peq.data();
|
|
565
|
+
|
|
566
|
+
const bool has_cutoff = cutoff != kNoCutoff;
|
|
567
|
+
const std::size_t bail_threshold = m + n + (has_cutoff ? cutoff : 0U);
|
|
568
|
+
|
|
569
|
+
std::size_t j = 0;
|
|
570
|
+
for (const std::uint8_t c : text) {
|
|
571
|
+
const std::uint64_t* peq_row = peq_base + static_cast<std::size_t>(c) * K;
|
|
572
|
+
|
|
573
|
+
// Block 0: no carry in.
|
|
574
|
+
const std::uint64_t U0 = V0 & peq_row[0];
|
|
575
|
+
std::uint64_t sum0;
|
|
576
|
+
const bool carry01 = __builtin_add_overflow(V0, U0, &sum0);
|
|
577
|
+
V0 = (sum0 | (V0 - U0)) & mask0;
|
|
578
|
+
|
|
579
|
+
// Block 1: carry in from block 0, carry out to block 2.
|
|
580
|
+
const std::uint64_t U1 = V1 & peq_row[1];
|
|
581
|
+
std::uint64_t sum1a;
|
|
582
|
+
const bool carry_a = __builtin_add_overflow(V1, U1, &sum1a);
|
|
583
|
+
std::uint64_t sum1;
|
|
584
|
+
const bool carry_b =
|
|
585
|
+
__builtin_add_overflow(sum1a, static_cast<std::uint64_t>(carry01), &sum1);
|
|
586
|
+
const bool carry12 = carry_a || carry_b;
|
|
587
|
+
V1 = (sum1 | (V1 - U1)) & mask1;
|
|
588
|
+
|
|
589
|
+
// Block 2: carry in from block 1; no carry out (K = 3, last block).
|
|
590
|
+
const std::uint64_t U2 = V2 & peq_row[2];
|
|
591
|
+
std::uint64_t sum2;
|
|
592
|
+
__builtin_add_overflow(V2, U2, &sum2);
|
|
593
|
+
sum2 += static_cast<std::uint64_t>(carry12);
|
|
594
|
+
V2 = (sum2 | (V2 - U2)) & mask2;
|
|
595
|
+
|
|
596
|
+
++j;
|
|
597
|
+
if (has_cutoff) {
|
|
598
|
+
const std::size_t pc =
|
|
599
|
+
static_cast<std::size_t>(std::popcount(V0)) +
|
|
600
|
+
static_cast<std::size_t>(std::popcount(V1)) +
|
|
601
|
+
static_cast<std::size_t>(std::popcount(V2));
|
|
602
|
+
const std::size_t lower_bound_score = 2U * (j + pc);
|
|
603
|
+
if (lower_bound_score > bail_threshold) {
|
|
604
|
+
return cutoff + 1U;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const std::size_t lcs_unmatched =
|
|
610
|
+
static_cast<std::size_t>(std::popcount(V0)) +
|
|
611
|
+
static_cast<std::size_t>(std::popcount(V1)) +
|
|
612
|
+
static_cast<std::size_t>(std::popcount(V2));
|
|
613
|
+
const std::size_t lcs = m - lcs_unmatched;
|
|
614
|
+
return m + n - 2U * lcs;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// K = 4 hand-specialisation for ``193 <= m <= 256``. Four register-
|
|
618
|
+
// pinned blocks, three-stage carry chain. Beyond K = 4 the register
|
|
619
|
+
// pressure on x86-64 starts to spill V into stack, so the dispatch
|
|
620
|
+
// switches to the generic kernel.
|
|
621
|
+
inline std::size_t indel_distance_k4_u8(
|
|
622
|
+
std::span<const std::uint8_t> pattern,
|
|
623
|
+
std::span<const std::uint8_t> text,
|
|
624
|
+
std::size_t cutoff) noexcept {
|
|
625
|
+
const std::size_t m = pattern.size();
|
|
626
|
+
const std::size_t n = text.size();
|
|
627
|
+
if (m == 0U) return n;
|
|
628
|
+
if (n == 0U) return m;
|
|
629
|
+
// Caller guarantees 192 < m <= 256.
|
|
630
|
+
constexpr std::size_t K = 4;
|
|
631
|
+
const std::uint64_t one = 1U;
|
|
632
|
+
|
|
633
|
+
MultiWordU8Scratch& scr = multi_word_u8_scratch();
|
|
634
|
+
scr.peq_build(pattern, K);
|
|
635
|
+
|
|
636
|
+
const std::uint64_t mask0 = ~std::uint64_t{0};
|
|
637
|
+
const std::uint64_t mask1 = ~std::uint64_t{0};
|
|
638
|
+
const std::uint64_t mask2 = ~std::uint64_t{0};
|
|
639
|
+
const std::uint64_t mask3 =
|
|
640
|
+
(m % 64U == 0U) ? ~std::uint64_t{0} : ((one << (m % 64U)) - 1U);
|
|
641
|
+
|
|
642
|
+
std::uint64_t V0 = mask0;
|
|
643
|
+
std::uint64_t V1 = mask1;
|
|
644
|
+
std::uint64_t V2 = mask2;
|
|
645
|
+
std::uint64_t V3 = mask3;
|
|
646
|
+
|
|
647
|
+
const std::uint64_t* const peq_base = scr.peq.data();
|
|
648
|
+
|
|
649
|
+
const bool has_cutoff = cutoff != kNoCutoff;
|
|
650
|
+
const std::size_t bail_threshold = m + n + (has_cutoff ? cutoff : 0U);
|
|
651
|
+
|
|
652
|
+
std::size_t j = 0;
|
|
653
|
+
for (const std::uint8_t c : text) {
|
|
654
|
+
const std::uint64_t* peq_row = peq_base + static_cast<std::size_t>(c) * K;
|
|
655
|
+
|
|
656
|
+
// Block 0: no carry in.
|
|
657
|
+
const std::uint64_t U0 = V0 & peq_row[0];
|
|
658
|
+
std::uint64_t sum0;
|
|
659
|
+
const bool carry01 = __builtin_add_overflow(V0, U0, &sum0);
|
|
660
|
+
V0 = (sum0 | (V0 - U0)) & mask0;
|
|
661
|
+
|
|
662
|
+
// Block 1: carry in from block 0, carry out to block 2.
|
|
663
|
+
const std::uint64_t U1 = V1 & peq_row[1];
|
|
664
|
+
std::uint64_t sum1a;
|
|
665
|
+
const bool c1a = __builtin_add_overflow(V1, U1, &sum1a);
|
|
666
|
+
std::uint64_t sum1;
|
|
667
|
+
const bool c1b =
|
|
668
|
+
__builtin_add_overflow(sum1a, static_cast<std::uint64_t>(carry01), &sum1);
|
|
669
|
+
const bool carry12 = c1a || c1b;
|
|
670
|
+
V1 = (sum1 | (V1 - U1)) & mask1;
|
|
671
|
+
|
|
672
|
+
// Block 2: carry in from block 1, carry out to block 3.
|
|
673
|
+
const std::uint64_t U2 = V2 & peq_row[2];
|
|
674
|
+
std::uint64_t sum2a;
|
|
675
|
+
const bool c2a = __builtin_add_overflow(V2, U2, &sum2a);
|
|
676
|
+
std::uint64_t sum2;
|
|
677
|
+
const bool c2b =
|
|
678
|
+
__builtin_add_overflow(sum2a, static_cast<std::uint64_t>(carry12), &sum2);
|
|
679
|
+
const bool carry23 = c2a || c2b;
|
|
680
|
+
V2 = (sum2 | (V2 - U2)) & mask2;
|
|
681
|
+
|
|
682
|
+
// Block 3: carry in from block 2; no carry out (K = 4, last block).
|
|
683
|
+
const std::uint64_t U3 = V3 & peq_row[3];
|
|
684
|
+
std::uint64_t sum3;
|
|
685
|
+
__builtin_add_overflow(V3, U3, &sum3);
|
|
686
|
+
sum3 += static_cast<std::uint64_t>(carry23);
|
|
687
|
+
V3 = (sum3 | (V3 - U3)) & mask3;
|
|
688
|
+
|
|
689
|
+
++j;
|
|
690
|
+
if (has_cutoff) {
|
|
691
|
+
const std::size_t pc =
|
|
692
|
+
static_cast<std::size_t>(std::popcount(V0)) +
|
|
693
|
+
static_cast<std::size_t>(std::popcount(V1)) +
|
|
694
|
+
static_cast<std::size_t>(std::popcount(V2)) +
|
|
695
|
+
static_cast<std::size_t>(std::popcount(V3));
|
|
696
|
+
const std::size_t lower_bound_score = 2U * (j + pc);
|
|
697
|
+
if (lower_bound_score > bail_threshold) {
|
|
698
|
+
return cutoff + 1U;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const std::size_t lcs_unmatched =
|
|
704
|
+
static_cast<std::size_t>(std::popcount(V0)) +
|
|
705
|
+
static_cast<std::size_t>(std::popcount(V1)) +
|
|
706
|
+
static_cast<std::size_t>(std::popcount(V2)) +
|
|
707
|
+
static_cast<std::size_t>(std::popcount(V3));
|
|
708
|
+
const std::size_t lcs = m - lcs_unmatched;
|
|
709
|
+
return m + n - 2U * lcs;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Templated K = 5..8 implementation. ``K`` is a compile-time
|
|
713
|
+
// constant, so the per-block loop body unrolls and V[0..K-1] /
|
|
714
|
+
// mask[0..K-1] become register-resident stack arrays the compiler can
|
|
715
|
+
// keep in GPRs across the full text scan. Same fused single-step
|
|
716
|
+
// Hyyrö recurrence as the K = 2/3/4 specialisations — kept in one
|
|
717
|
+
// templated function instead of four hand-expanded variants because
|
|
718
|
+
// the duplication offers no clarity at that point.
|
|
719
|
+
//
|
|
720
|
+
// Above K = 8 the register file (16 GPRs on x86-64) overflows and the
|
|
721
|
+
// templated form's advantage disappears; for K >= 9 the dispatch
|
|
722
|
+
// falls back to ``indel_distance_multi_word_u8``, which uses the same
|
|
723
|
+
// fused-recurrence body but with a runtime K loop and a heap-backed
|
|
724
|
+
// V / mask buffer.
|
|
725
|
+
template <std::size_t K>
|
|
726
|
+
inline std::size_t indel_distance_kN_u8(
|
|
727
|
+
std::span<const std::uint8_t> pattern,
|
|
728
|
+
std::span<const std::uint8_t> text,
|
|
729
|
+
std::size_t cutoff) noexcept {
|
|
730
|
+
static_assert(K >= 2 && K <= 8, "indel_distance_kN_u8 covers K=2..8");
|
|
731
|
+
const std::size_t m = pattern.size();
|
|
732
|
+
const std::size_t n = text.size();
|
|
733
|
+
if (m == 0U) return n;
|
|
734
|
+
if (n == 0U) return m;
|
|
735
|
+
if (cutoff != kNoCutoff) {
|
|
736
|
+
const std::size_t len_diff = m > n ? m - n : n - m;
|
|
737
|
+
if (len_diff > cutoff) return cutoff + 1U;
|
|
738
|
+
}
|
|
739
|
+
const std::uint64_t one = 1U;
|
|
740
|
+
|
|
741
|
+
MultiWordU8Scratch& scr = multi_word_u8_scratch();
|
|
742
|
+
scr.peq_build(pattern, K);
|
|
743
|
+
|
|
744
|
+
// Stack-resident per-block state. ``K`` is constexpr so these are
|
|
745
|
+
// fixed-size arrays the compiler can promote to registers.
|
|
746
|
+
std::uint64_t mask[K];
|
|
747
|
+
for (std::size_t k = 0; k < K - 1U; ++k) mask[k] = ~std::uint64_t{0};
|
|
748
|
+
mask[K - 1U] =
|
|
749
|
+
(m % 64U == 0U) ? ~std::uint64_t{0} : ((one << (m % 64U)) - 1U);
|
|
750
|
+
|
|
751
|
+
std::uint64_t V[K];
|
|
752
|
+
for (std::size_t k = 0; k < K; ++k) V[k] = mask[k];
|
|
753
|
+
|
|
754
|
+
const std::uint64_t* const peq_base = scr.peq.data();
|
|
755
|
+
|
|
756
|
+
const bool has_cutoff = cutoff != kNoCutoff;
|
|
757
|
+
const std::size_t bail_threshold = m + n + (has_cutoff ? cutoff : 0U);
|
|
758
|
+
|
|
759
|
+
std::size_t j = 0;
|
|
760
|
+
for (const std::uint8_t c : text) {
|
|
761
|
+
const std::uint64_t* peq_row = peq_base + static_cast<std::size_t>(c) * K;
|
|
762
|
+
|
|
763
|
+
// Block 0: no carry in. Carry-out feeds the next block.
|
|
764
|
+
std::uint64_t carry;
|
|
765
|
+
{
|
|
766
|
+
const std::uint64_t U = V[0] & peq_row[0];
|
|
767
|
+
std::uint64_t s;
|
|
768
|
+
const bool c1 = __builtin_add_overflow(V[0], U, &s);
|
|
769
|
+
carry = static_cast<std::uint64_t>(c1);
|
|
770
|
+
V[0] = (s | (V[0] - U)) & mask[0];
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Middle blocks: carry in and carry out. The loop is unrolled by
|
|
774
|
+
// the compiler because ``K`` is constexpr; the carry is a single
|
|
775
|
+
// ``uint64_t`` (always 0 or 1 by the Hyyrö U <= V invariant) so
|
|
776
|
+
// the chain lowers to ADCX/ADOX where available.
|
|
777
|
+
for (std::size_t k = 1; k < K - 1U; ++k) {
|
|
778
|
+
const std::uint64_t U = V[k] & peq_row[k];
|
|
779
|
+
std::uint64_t s1, s2;
|
|
780
|
+
const bool c1 = __builtin_add_overflow(V[k], U, &s1);
|
|
781
|
+
const bool c2 = __builtin_add_overflow(s1, carry, &s2);
|
|
782
|
+
carry = static_cast<std::uint64_t>(c1) + static_cast<std::uint64_t>(c2);
|
|
783
|
+
V[k] = (s2 | (V[k] - U)) & mask[k];
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// Last block: carry in, no carry out needed.
|
|
787
|
+
{
|
|
788
|
+
constexpr std::size_t k = K - 1U;
|
|
789
|
+
const std::uint64_t U = V[k] & peq_row[k];
|
|
790
|
+
std::uint64_t s;
|
|
791
|
+
__builtin_add_overflow(V[k], U, &s);
|
|
792
|
+
s += carry;
|
|
793
|
+
V[k] = (s | (V[k] - U)) & mask[k];
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
++j;
|
|
797
|
+
if (has_cutoff) {
|
|
798
|
+
std::size_t pc = 0;
|
|
799
|
+
for (std::size_t k = 0; k < K; ++k) {
|
|
800
|
+
pc += static_cast<std::size_t>(std::popcount(V[k]));
|
|
801
|
+
}
|
|
802
|
+
const std::size_t lower_bound_score = 2U * (j + pc);
|
|
803
|
+
if (lower_bound_score > bail_threshold) {
|
|
804
|
+
return cutoff + 1U;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
std::size_t lcs_unmatched = 0;
|
|
810
|
+
for (std::size_t k = 0; k < K; ++k) {
|
|
811
|
+
lcs_unmatched += static_cast<std::size_t>(std::popcount(V[k]));
|
|
812
|
+
}
|
|
813
|
+
const std::size_t lcs = m - lcs_unmatched;
|
|
814
|
+
return m + n - 2U * lcs;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
inline std::size_t indel_distance_k5_u8(
|
|
818
|
+
std::span<const std::uint8_t> pattern,
|
|
819
|
+
std::span<const std::uint8_t> text,
|
|
820
|
+
std::size_t cutoff) noexcept {
|
|
821
|
+
return indel_distance_kN_u8<5>(pattern, text, cutoff);
|
|
822
|
+
}
|
|
823
|
+
inline std::size_t indel_distance_k6_u8(
|
|
824
|
+
std::span<const std::uint8_t> pattern,
|
|
825
|
+
std::span<const std::uint8_t> text,
|
|
826
|
+
std::size_t cutoff) noexcept {
|
|
827
|
+
return indel_distance_kN_u8<6>(pattern, text, cutoff);
|
|
828
|
+
}
|
|
829
|
+
inline std::size_t indel_distance_k7_u8(
|
|
830
|
+
std::span<const std::uint8_t> pattern,
|
|
831
|
+
std::span<const std::uint8_t> text,
|
|
832
|
+
std::size_t cutoff) noexcept {
|
|
833
|
+
return indel_distance_kN_u8<7>(pattern, text, cutoff);
|
|
834
|
+
}
|
|
835
|
+
inline std::size_t indel_distance_k8_u8(
|
|
836
|
+
std::span<const std::uint8_t> pattern,
|
|
837
|
+
std::span<const std::uint8_t> text,
|
|
838
|
+
std::size_t cutoff) noexcept {
|
|
839
|
+
return indel_distance_kN_u8<8>(pattern, text, cutoff);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Multi-word bit-parallel Indel for patterns of arbitrary length.
|
|
843
|
+
//
|
|
844
|
+
// Hyyrö (2004) generalisation: split the pattern into ``K = ceil(m / 64)``
|
|
845
|
+
// blocks of 64 bits each. The V vector becomes ``V[K]``; the per-text-
|
|
846
|
+
// character recurrence
|
|
847
|
+
// sum[k] = V[k] + (V[k] & PEQ[c][k]) (with carry between blocks)
|
|
848
|
+
// diff[k] = V[k] - (V[k] & PEQ[c][k]) (with borrow between blocks)
|
|
849
|
+
// V'[k] = (sum[k] | diff[k]) & mask[k]
|
|
850
|
+
// extends the single-word formula. The last block's ``mask`` masks
|
|
851
|
+
// off the bits beyond ``m % 64``.
|
|
852
|
+
//
|
|
853
|
+
// Memory: O(K) per call for the V buffer plus K-entry PEQ per
|
|
854
|
+
// observed alphabet character. PEQ is a hashmap for arbitrary
|
|
855
|
+
// ``Token``; the byte-alphabet specialisation stores a flat
|
|
856
|
+
// ``[256][K]`` array (rebuilt as a flat ``[256 * K]`` vector here so
|
|
857
|
+
// the same code path serves both).
|
|
858
|
+
template <typename Token>
|
|
859
|
+
inline std::size_t indel_distance_multi_word(
|
|
860
|
+
std::span<const Token> pattern,
|
|
861
|
+
std::span<const Token> text,
|
|
862
|
+
std::size_t cutoff) {
|
|
863
|
+
static_assert(std::is_integral_v<Token> || std::is_unsigned_v<Token>);
|
|
864
|
+
const std::size_t m = pattern.size();
|
|
865
|
+
const std::size_t n = text.size();
|
|
866
|
+
if (m == 0U) return n;
|
|
867
|
+
if (n == 0U) return m;
|
|
868
|
+
const std::size_t K = (m + 63U) / 64U;
|
|
869
|
+
const std::uint64_t one = 1U;
|
|
870
|
+
|
|
871
|
+
// PEQ as ``Token -> std::vector<uint64_t>(K)``.
|
|
872
|
+
std::unordered_map<Token, std::vector<std::uint64_t>> peq;
|
|
873
|
+
for (std::size_t i = 0; i < m; ++i) {
|
|
874
|
+
auto [it, inserted] = peq.try_emplace(pattern[i], K, std::uint64_t{0});
|
|
875
|
+
it->second[i >> 6U] |= one << (i & 63U);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// Per-block masks: full ``~0`` for all but the last block, which
|
|
879
|
+
// masks off the bits at indices >= ``m``.
|
|
880
|
+
std::vector<std::uint64_t> mask(K, ~std::uint64_t{0});
|
|
881
|
+
if (m % 64U != 0U) {
|
|
882
|
+
mask.back() = (one << (m % 64U)) - 1U;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// V starts as the all-ones pattern (every position "unmatched").
|
|
886
|
+
std::vector<std::uint64_t> V(K);
|
|
887
|
+
for (std::size_t k = 0; k < K; ++k) V[k] = mask[k];
|
|
888
|
+
|
|
889
|
+
std::vector<std::uint64_t> sum(K), diff(K), U(K);
|
|
890
|
+
|
|
891
|
+
// Cutoff bookkeeping. The single-word lower bound generalises:
|
|
892
|
+
// popcount(V) is summed across blocks.
|
|
893
|
+
const bool has_cutoff = cutoff != kNoCutoff;
|
|
894
|
+
const std::size_t bail_threshold = m + n + (has_cutoff ? cutoff : 0U);
|
|
895
|
+
|
|
896
|
+
std::size_t j = 0;
|
|
897
|
+
for (const auto c : text) {
|
|
898
|
+
const auto it = peq.find(c);
|
|
899
|
+
const std::uint64_t* peq_row = (it == peq.end()) ? nullptr : it->second.data();
|
|
900
|
+
|
|
901
|
+
// U[k] = V[k] & PEQ[c][k]
|
|
902
|
+
for (std::size_t k = 0; k < K; ++k) {
|
|
903
|
+
U[k] = peq_row == nullptr ? std::uint64_t{0} : V[k] & peq_row[k];
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// sum[k] = V[k] + U[k] with carry between blocks.
|
|
907
|
+
std::uint64_t carry = 0;
|
|
908
|
+
for (std::size_t k = 0; k < K; ++k) {
|
|
909
|
+
const std::uint64_t a = V[k];
|
|
910
|
+
const std::uint64_t b = U[k];
|
|
911
|
+
const std::uint64_t s1 = a + b;
|
|
912
|
+
const std::uint64_t s2 = s1 + carry;
|
|
913
|
+
// Carry out: 1 if s1 < a (overflow on a+b) or (carry && s2 < s1).
|
|
914
|
+
carry = static_cast<std::uint64_t>(s1 < a) +
|
|
915
|
+
static_cast<std::uint64_t>(s2 < s1);
|
|
916
|
+
sum[k] = s2;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// diff[k] = V[k] - U[k] with borrow between blocks.
|
|
920
|
+
std::uint64_t borrow = 0;
|
|
921
|
+
for (std::size_t k = 0; k < K; ++k) {
|
|
922
|
+
const std::uint64_t a = V[k];
|
|
923
|
+
const std::uint64_t b = U[k];
|
|
924
|
+
const std::uint64_t d1 = a - b;
|
|
925
|
+
const std::uint64_t d2 = d1 - borrow;
|
|
926
|
+
// Borrow out: 1 if a < b OR (borrow && d1 == 0 && borrow > 0).
|
|
927
|
+
borrow = static_cast<std::uint64_t>(a < b) +
|
|
928
|
+
static_cast<std::uint64_t>(d1 < borrow);
|
|
929
|
+
diff[k] = d2;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// V'[k] = (sum[k] | diff[k]) & mask[k]
|
|
933
|
+
for (std::size_t k = 0; k < K; ++k) {
|
|
934
|
+
V[k] = (sum[k] | diff[k]) & mask[k];
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
++j;
|
|
938
|
+
if (has_cutoff) {
|
|
939
|
+
std::size_t pc = 0;
|
|
940
|
+
for (std::size_t k = 0; k < K; ++k) {
|
|
941
|
+
pc += static_cast<std::size_t>(std::popcount(V[k]));
|
|
942
|
+
}
|
|
943
|
+
const std::size_t lower_bound_score = 2U * (j + pc);
|
|
944
|
+
if (lower_bound_score > bail_threshold) {
|
|
945
|
+
return cutoff + 1U;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
std::size_t lcs_unmatched = 0;
|
|
951
|
+
for (std::size_t k = 0; k < K; ++k) {
|
|
952
|
+
lcs_unmatched += static_cast<std::size_t>(std::popcount(V[k]));
|
|
953
|
+
}
|
|
954
|
+
const std::size_t lcs = m - lcs_unmatched;
|
|
955
|
+
return m + n - 2U * lcs;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// Prepared bit-parallel pattern — PEQ built once, reused across many
|
|
959
|
+
// targets. The win is concentrated in batch / multi-window workloads
|
|
960
|
+
// (``partial_ratio`` evaluates the same pattern against multiple
|
|
961
|
+
// window slices) where the PEQ build cost would otherwise be paid
|
|
962
|
+
// per evaluation.
|
|
963
|
+
template <typename Token>
|
|
964
|
+
struct PreparedIndelPattern {
|
|
965
|
+
static constexpr bool kIsByte = std::is_same_v<Token, std::uint8_t>;
|
|
966
|
+
// Storage: 256-entry array for byte-alphabet (the indel_single_word_u8
|
|
967
|
+
// shape), arbitrary hashmap otherwise. Both are queried the same way
|
|
968
|
+
// via ``peq_of(c)``.
|
|
969
|
+
std::conditional_t<kIsByte,
|
|
970
|
+
std::array<std::uint64_t, 256>,
|
|
971
|
+
std::unordered_map<Token, std::uint64_t>> peq{};
|
|
972
|
+
std::uint64_t mask = 0;
|
|
973
|
+
std::size_t m = 0;
|
|
974
|
+
bool use_dp = false; // true when m > 64; falls back to scalar DP
|
|
975
|
+
std::span<const Token> pattern{}; // retained for the DP fallback path
|
|
976
|
+
|
|
977
|
+
std::uint64_t peq_of(Token c) const noexcept {
|
|
978
|
+
if constexpr (kIsByte) {
|
|
979
|
+
return peq[c];
|
|
980
|
+
} else {
|
|
981
|
+
const auto it = peq.find(c);
|
|
982
|
+
return it == peq.end() ? std::uint64_t{0} : it->second;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
};
|
|
986
|
+
|
|
987
|
+
template <typename Token>
|
|
988
|
+
inline PreparedIndelPattern<Token> prepare_indel_pattern(
|
|
989
|
+
std::span<const Token> pattern) {
|
|
990
|
+
static_assert(std::is_integral_v<Token> || std::is_unsigned_v<Token>);
|
|
991
|
+
PreparedIndelPattern<Token> prepared;
|
|
992
|
+
prepared.m = pattern.size();
|
|
993
|
+
prepared.pattern = pattern;
|
|
994
|
+
if (prepared.m == 0U || prepared.m > 64U) {
|
|
995
|
+
// For empty or multi-word patterns we don't pre-build the single-
|
|
996
|
+
// word PEQ; the ``indel_distance_prepared`` path detects this and
|
|
997
|
+
// routes to the multi-word kernel (which builds its own per-block
|
|
998
|
+
// PEQ inline — see below for the prepared multi-word variant).
|
|
999
|
+
prepared.use_dp = true;
|
|
1000
|
+
return prepared;
|
|
1001
|
+
}
|
|
1002
|
+
const std::uint64_t one = 1U;
|
|
1003
|
+
for (std::size_t i = 0; i < prepared.m; ++i) {
|
|
1004
|
+
if constexpr (PreparedIndelPattern<Token>::kIsByte) {
|
|
1005
|
+
prepared.peq[pattern[i]] |= one << i;
|
|
1006
|
+
} else {
|
|
1007
|
+
prepared.peq[pattern[i]] |= one << i;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
prepared.mask =
|
|
1011
|
+
(prepared.m == 64U) ? ~std::uint64_t{0} : ((one << prepared.m) - 1U);
|
|
1012
|
+
return prepared;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// Multi-word kernel that accepts a pre-built PEQ buffer instead of
|
|
1016
|
+
// rebuilding it from the pattern. The buffer layout is the standard
|
|
1017
|
+
// flat ``[256][K]`` row-major: ``peq[c * K + b]`` is block ``b`` of
|
|
1018
|
+
// the bitmap for character ``c``. Used by ``partial_ratio`` (and
|
|
1019
|
+
// other "fixed pattern, many text windows" loops) to amortise the
|
|
1020
|
+
// 256 * K * 8-byte PEQ fill across many calls.
|
|
1021
|
+
//
|
|
1022
|
+
// Carry chain + cutoff bookkeeping match ``indel_distance_multi_word_u8``;
|
|
1023
|
+
// only the PEQ source changes.
|
|
1024
|
+
inline std::size_t indel_distance_multi_word_u8_with_peq(
|
|
1025
|
+
const std::uint64_t* peq, std::size_t K,
|
|
1026
|
+
std::size_t m, // pattern length
|
|
1027
|
+
std::span<const std::uint8_t> text,
|
|
1028
|
+
std::size_t cutoff = kNoCutoff) {
|
|
1029
|
+
const std::size_t n = text.size();
|
|
1030
|
+
if (m == 0U) return n;
|
|
1031
|
+
if (n == 0U) return m;
|
|
1032
|
+
if (cutoff != kNoCutoff) {
|
|
1033
|
+
const std::size_t len_diff = m > n ? m - n : n - m;
|
|
1034
|
+
if (len_diff > cutoff) return cutoff + 1U;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// Stack-resident V[K] when K small (K <= 8 covers up to m = 512;
|
|
1038
|
+
// partial_ratio's hot path is K <= 4). Heap fallback for K > 8.
|
|
1039
|
+
constexpr std::size_t kStackMaxK = 8U;
|
|
1040
|
+
std::uint64_t V_stack[kStackMaxK];
|
|
1041
|
+
std::vector<std::uint64_t> V_heap;
|
|
1042
|
+
std::uint64_t* V;
|
|
1043
|
+
if (K <= kStackMaxK) {
|
|
1044
|
+
V = V_stack;
|
|
1045
|
+
} else {
|
|
1046
|
+
V_heap.resize(K);
|
|
1047
|
+
V = V_heap.data();
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
const std::uint64_t one = 1U;
|
|
1051
|
+
const std::size_t last_bits = m - (K - 1U) * 64U;
|
|
1052
|
+
const std::uint64_t mask_last =
|
|
1053
|
+
(last_bits == 64U) ? ~std::uint64_t{0} : ((one << last_bits) - 1U);
|
|
1054
|
+
for (std::size_t k = 0; k < K - 1U; ++k) V[k] = ~std::uint64_t{0};
|
|
1055
|
+
V[K - 1U] = mask_last;
|
|
1056
|
+
|
|
1057
|
+
const bool has_cutoff = cutoff != kNoCutoff;
|
|
1058
|
+
const std::size_t bail_threshold = m + n + (has_cutoff ? cutoff : 0U);
|
|
1059
|
+
|
|
1060
|
+
std::size_t j = 0;
|
|
1061
|
+
for (const std::uint8_t c : text) {
|
|
1062
|
+
const std::uint64_t* peq_row = peq + static_cast<std::size_t>(c) * K;
|
|
1063
|
+
std::uint64_t carry_in = 0;
|
|
1064
|
+
for (std::size_t k = 0; k < K; ++k) {
|
|
1065
|
+
const std::uint64_t Vk = V[k];
|
|
1066
|
+
const std::uint64_t Uk = Vk & peq_row[k];
|
|
1067
|
+
std::uint64_t s1;
|
|
1068
|
+
const bool c1 = __builtin_add_overflow(Vk, Uk, &s1);
|
|
1069
|
+
std::uint64_t s2;
|
|
1070
|
+
const bool c2 = __builtin_add_overflow(s1, carry_in, &s2);
|
|
1071
|
+
carry_in = static_cast<std::uint64_t>(c1) +
|
|
1072
|
+
static_cast<std::uint64_t>(c2);
|
|
1073
|
+
const std::uint64_t mask_k =
|
|
1074
|
+
(k == K - 1U) ? mask_last : ~std::uint64_t{0};
|
|
1075
|
+
V[k] = (s2 | (Vk - Uk)) & mask_k;
|
|
1076
|
+
}
|
|
1077
|
+
++j;
|
|
1078
|
+
if (has_cutoff) {
|
|
1079
|
+
std::size_t pc = 0;
|
|
1080
|
+
for (std::size_t k = 0; k < K; ++k) {
|
|
1081
|
+
pc += static_cast<std::size_t>(std::popcount(V[k]));
|
|
1082
|
+
}
|
|
1083
|
+
const std::size_t lower_bound_score = 2U * (j + pc);
|
|
1084
|
+
if (lower_bound_score > bail_threshold) {
|
|
1085
|
+
return cutoff + 1U;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
std::size_t lcs_unmatched = 0;
|
|
1090
|
+
for (std::size_t k = 0; k < K; ++k) {
|
|
1091
|
+
lcs_unmatched += static_cast<std::size_t>(std::popcount(V[k]));
|
|
1092
|
+
}
|
|
1093
|
+
return m + n - 2U * (m - lcs_unmatched);
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// Build a multi-word PEQ for a byte-pattern into a caller-provided
|
|
1097
|
+
// vector (sized 256 * K). Uses dirty-symbol clearing when the same
|
|
1098
|
+
// buffer is reused at the same K (partial_ratio's thread-local peq).
|
|
1099
|
+
inline void build_multi_word_peq_u8(
|
|
1100
|
+
std::span<const std::uint8_t> pattern,
|
|
1101
|
+
std::size_t K,
|
|
1102
|
+
std::vector<std::uint64_t>& peq) {
|
|
1103
|
+
// Per-thread dirty bookkeeping for the external peq buffer. Assumes
|
|
1104
|
+
// a single persistent buffer per thread (the partial_ratio path).
|
|
1105
|
+
thread_local std::vector<std::uint8_t> dirty;
|
|
1106
|
+
thread_local std::array<std::uint32_t, 256> touch{};
|
|
1107
|
+
thread_local std::uint32_t gen = 0;
|
|
1108
|
+
thread_local std::size_t layout_k = 0;
|
|
1109
|
+
|
|
1110
|
+
if (peq.size() < 256U * K) {
|
|
1111
|
+
peq.assign(256U * K, 0U);
|
|
1112
|
+
dirty.clear();
|
|
1113
|
+
layout_k = K;
|
|
1114
|
+
touch.fill(0);
|
|
1115
|
+
gen = 0;
|
|
1116
|
+
} else if (layout_k != K) {
|
|
1117
|
+
std::fill_n(peq.data(), 256U * K, std::uint64_t{0});
|
|
1118
|
+
dirty.clear();
|
|
1119
|
+
layout_k = K;
|
|
1120
|
+
} else {
|
|
1121
|
+
for (const std::uint8_t s : dirty) {
|
|
1122
|
+
std::fill_n(
|
|
1123
|
+
peq.data() + static_cast<std::size_t>(s) * K, K, std::uint64_t{0});
|
|
1124
|
+
}
|
|
1125
|
+
dirty.clear();
|
|
1126
|
+
}
|
|
1127
|
+
if (++gen == 0U) {
|
|
1128
|
+
touch.fill(0);
|
|
1129
|
+
gen = 1U;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
for (std::size_t i = 0; i < pattern.size(); ++i) {
|
|
1133
|
+
const std::uint8_t c = pattern[i];
|
|
1134
|
+
if (touch[c] != gen) {
|
|
1135
|
+
touch[c] = gen;
|
|
1136
|
+
dirty.push_back(c);
|
|
1137
|
+
}
|
|
1138
|
+
peq[static_cast<std::size_t>(c) * K + (i >> 6U)] |=
|
|
1139
|
+
std::uint64_t{1} << (i & 63U);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
template <typename Token>
|
|
1144
|
+
inline std::size_t indel_distance_prepared(
|
|
1145
|
+
const PreparedIndelPattern<Token>& prepared,
|
|
1146
|
+
std::span<const Token> text,
|
|
1147
|
+
std::size_t cutoff = kNoCutoff) {
|
|
1148
|
+
const std::size_t m = prepared.m;
|
|
1149
|
+
const std::size_t n = text.size();
|
|
1150
|
+
if (m == 0U) return n;
|
|
1151
|
+
if (n == 0U) return m;
|
|
1152
|
+
if (cutoff != kNoCutoff) {
|
|
1153
|
+
const std::size_t len_diff = m > n ? m - n : n - m;
|
|
1154
|
+
if (len_diff > cutoff) return cutoff + 1U;
|
|
1155
|
+
}
|
|
1156
|
+
if (prepared.use_dp) {
|
|
1157
|
+
// Pattern too long for the single-word prepared PEQ: route to
|
|
1158
|
+
// the multi-word kernel. The multi-word PEQ build is amortised
|
|
1159
|
+
// over the text traversal already; pre-building it would require
|
|
1160
|
+
// a separate prepared-multi-word struct (TODO if profiling shows
|
|
1161
|
+
// it matters).
|
|
1162
|
+
return indel_distance_multi_word<Token>(prepared.pattern, text, cutoff);
|
|
1163
|
+
}
|
|
1164
|
+
std::uint64_t V = prepared.mask;
|
|
1165
|
+
if (cutoff == kNoCutoff) {
|
|
1166
|
+
for (const auto c : text) {
|
|
1167
|
+
const std::uint64_t U = V & prepared.peq_of(c);
|
|
1168
|
+
V = ((V + U) | (V - U)) & prepared.mask;
|
|
1169
|
+
}
|
|
1170
|
+
} else {
|
|
1171
|
+
const std::size_t bail_threshold = m + n + cutoff;
|
|
1172
|
+
std::size_t j = 0;
|
|
1173
|
+
for (const auto c : text) {
|
|
1174
|
+
const std::uint64_t U = V & prepared.peq_of(c);
|
|
1175
|
+
V = ((V + U) | (V - U)) & prepared.mask;
|
|
1176
|
+
++j;
|
|
1177
|
+
const std::size_t lower_bound_score =
|
|
1178
|
+
2U * (j + static_cast<std::size_t>(std::popcount(V)));
|
|
1179
|
+
if (lower_bound_score > bail_threshold) {
|
|
1180
|
+
return cutoff + 1U;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
const std::size_t lcs =
|
|
1185
|
+
m - static_cast<std::size_t>(std::popcount(V));
|
|
1186
|
+
return m + n - 2U * lcs;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
// Normalized similarity. The denominator is |a| + |b| (NOT
|
|
1190
|
+
// max(|a|, |b|) — Indel can be up to a_len + b_len, e.g.
|
|
1191
|
+
// indel("aaa", "bbb") = 6).
|
|
1192
|
+
inline double normalize(
|
|
1193
|
+
std::size_t distance, std::size_t a_len, std::size_t b_len) noexcept {
|
|
1194
|
+
const std::size_t total = a_len + b_len;
|
|
1195
|
+
if (total == 0U) return 1.0;
|
|
1196
|
+
return 1.0 -
|
|
1197
|
+
static_cast<double>(distance) / static_cast<double>(total);
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
} // namespace stride_align::indel
|