Everett
Loading...
Searching...
No Matches
fixed_search.h
Go to the documentation of this file.
1
13#pragma once
14
16#include <everett/backend.h>
17#include <simd/integer.h>
18
19#include <algorithm>
20#include <array>
21#include <bit>
22#include <cstddef>
23#include <cstdint>
24#include <cstring>
25#include <limits>
26#include <span>
27#include <stdexcept>
28
29namespace everett {
30 namespace fixed_search_detail {
31 inline std::uint32_t load_word(std::byte const *source) noexcept {
32 std::uint32_t value;
33 std::memcpy(&value, source, sizeof(value));
34 if constexpr (std::endian::native == std::endian::big) {
35 value = ((value & 0x00ff00ffu) << 8) | ((value >> 8) & 0x00ff00ffu);
36 value = (value << 16) | (value >> 16);
37 }
38 return value;
39 }
40
41 template <std::size_t Words, std::size_t Lanes>
42 inline std::array<std::uint32_t, Lanes> repeated(std::array<std::uint32_t, Words> const &query) noexcept {
43 std::array<std::uint32_t, Lanes> result{};
44 for (std::size_t i = 0; i < Lanes; ++i) result[i] = query[i % Words];
45 return result;
46 }
47
48 template <std::size_t Words, bool Upper>
49 inline bool precedes(std::byte const *source, std::array<std::uint32_t, Words> const &query) noexcept {
50 bool less = false, equal = true;
51 for (std::size_t i = 0; i < Words; ++i) {
52 auto value = load_word(source + (i << 2));
53 less |= equal & (value < query[i]);
54 equal &= value == query[i];
55 }
56 if constexpr (Upper) return less | equal;
57 else return less;
58 }
59
60 template <bool Upper, simd::architecture Arch>
61 simd_inline bool precedes_four(std::byte const *source, simd::vec<std::uint32_t, 4, Arch> query) noexcept {
62 using V = simd::vec<std::uint32_t, 4, Arch>;
63 auto value = V::load(reinterpret_cast<std::uint32_t const *>(source));
64 // A more significant unequal word outweighs every following word.
65 // Summing unsigned lane differences modulo 2^32 gives -15..15.
66 auto order = mask_bits(value > query) - mask_bits(value < query);
67 auto weighted = order * V(std::array<std::uint32_t, 4>{8, 4, 2, 1});
68 auto comparison = std::bit_cast<std::int32_t>(std::uint32_t(reduce_add_widened(weighted)));
69 if constexpr (Upper) return comparison >= 0;
70 else return comparison > 0;
71 }
72
73 // Count keys satisfying < query (or <= query), not individual word lanes.
74 // The first word of each key is the most significant comparison word.
75 // Callers supply at most four complete keys; no padding is required.
76 template <std::size_t Words, bool Upper, simd::architecture Arch>
77 simd_inline unsigned population(std::byte const *source, unsigned count,
78 std::array<std::uint32_t, Words> const &query) noexcept {
79 if (!count) return 0;
80 if constexpr (std::is_same_v<Arch, simd::scalar> || std::endian::native != std::endian::little) {
81 unsigned result = 0;
82 for (unsigned i = 0; i < count; ++i) result += precedes<Words, Upper>(source + i * Words * 4, query);
83 return result;
84 } else if constexpr (std::is_same_v<Arch, simd::neon>) {
85 if constexpr (Words == 1) {
86 using V = simd::vec<std::uint32_t, 4, Arch>;
87 auto values = V::load_partial(reinterpret_cast<std::uint32_t const *>(source), count);
88 auto queries = V(query[0]);
89 auto active = V(std::array<std::uint32_t, 4>{0, 1, 2, 3}) < V(count);
90 auto matches = Upper ? values <= queries : values < queries;
91 return unsigned(reduce_add_widened(select(matches & active, V(1), V(0))));
92 } else if constexpr (Words == 2) {
93 using W = simd::vec<std::uint32_t, 4, Arch>;
94 using V = simd::vec<std::uint64_t, 2, Arch>;
95 auto queries = V((std::uint64_t(query[0]) << 32) | query[1]);
96 unsigned result = 0;
97 for (unsigned at = 0; at < 4; at += 2) {
98 if (at >= count) break;
99 if (count - at == 1) result += precedes<Words, Upper>(source + (at << 3), query);
100 else {
101 auto words = W::load(reinterpret_cast<std::uint32_t const *>(source + (at << 3)));
102 W swapped = words.yxwz;
103 auto values = reinterpret_bits<std::uint64_t>(swapped);
104 auto matches = Upper ? values <= queries : values < queries;
105 auto counts = mask_bits(matches).template right<63>();
106 result += unsigned(reduce_add_widened(reinterpret_bits<std::uint32_t>(counts)));
107 }
108 }
109 return result;
110 } else {
111 auto queries = simd::vec<std::uint32_t, 4, Arch>::load(query.data());
112 unsigned result = 0;
113 for (unsigned at = 0; at < 4; ++at) {
114 if (at >= count) break;
115 result += precedes_four<Upper, Arch>(source + (at << 4), queries);
116 }
117 return result;
118 }
119 } else {
120 unsigned less = 0, equal = 0;
121 unsigned word_count = count * unsigned(Words);
122 constexpr unsigned lanes = unsigned(backend_detail::register_bytes<Arch> / sizeof(std::uint32_t));
123 using V = simd::vec<std::uint32_t, lanes, Arch>;
124 auto queries = V(repeated<Words, lanes>(query));
125 for (unsigned at = 0; at < Words * 4; at += lanes) {
126 if (at >= word_count) break;
127 auto values = V::load_partial(reinterpret_cast<std::uint32_t const *>(source + (at << 2)),
128 std::min(lanes, word_count - at));
129 less |= unsigned((values < queries).to_bitset()) << at;
130 equal |= unsigned((values == queries).to_bitset()) << at;
131 }
132 constexpr unsigned starts = Words == 1 ? 0xffffu : Words == 2 ? 0x5555u : 0x1111u;
133 unsigned prefix = starts & ((1u << word_count) - 1);
134 unsigned matches = prefix, result = 0;
135 for (unsigned word = 0; word < Words; ++word) {
136 result |= matches & (less >> word);
137 matches &= equal >> word;
138 }
139 if constexpr (Upper) result |= matches;
140 return unsigned(std::popcount(result));
141 }
142 }
143 }
144
145 // Borrows sorted keys encoded as one, two, or four little-endian uint32 words.
146 // Word zero is compared first; this is not a native-endian uint128 array.
147 // The owner must outlive the view. Construction checks extent, not sortedness.
148 // Bounds use local ordinals, permit duplicate keys and never allocate.
149 template <std::size_t Words, simd::architecture Arch = simd::scalar> struct fixed_key_view {
150 static_assert(Words == 1 || Words == 2 || Words == 4, "fixed keys have 1, 2, or 4 words");
151 using architecture = Arch;
152 using key_type = std::array<std::uint32_t, Words>;
153 static constexpr std::size_t key_bytes = Words * 4;
154 static constexpr std::size_t explicit_simd_limit = 32;
155 fixed_key_view() = default;
156 explicit fixed_key_view(std::span<std::byte const> bytes) : bytes_(bytes) {
157 if (bytes.size() % key_bytes)
158 error_detail::raise<std::invalid_argument>("fixed-key extent is not a whole number of keys");
159 }
160 std::size_t size() const noexcept { return bytes_.size() / key_bytes; }
161 bool empty() const noexcept { return bytes_.empty(); }
162 std::span<std::byte const> bytes() const noexcept { return bytes_; }
163 key_type key_at(std::size_t ordinal) const {
164 if (ordinal >= size()) error_detail::raise<std::out_of_range>("fixed-key ordinal");
165 key_type result;
166 auto source = bytes_.data() + ordinal * key_bytes;
167 for (std::size_t i = 0; i < Words; ++i) result[i] = fixed_search_detail::load_word(source + (i << 2));
168 return result;
169 }
170 fixed_key_view subview(std::size_t first, std::size_t count) const {
171 if (first > size() || count > size() - first)
172 error_detail::raise<std::out_of_range>("fixed-key subview");
173 return fixed_key_view(bytes_.subspan(first * key_bytes, count * key_bytes));
174 }
175 std::size_t lower_bound(key_type const &query) const noexcept {
176 return prefer_simd() ? simd_bound<false>(query) : binary_bound<false>(query);
177 }
178 std::size_t upper_bound(key_type const &query) const noexcept {
179 return prefer_simd() ? simd_bound<true>(query) : binary_bound<true>(query);
180 }
181 std::size_t lower_bound_binary(key_type const &query) const noexcept { return binary_bound<false>(query); }
182 std::size_t upper_bound_binary(key_type const &query) const noexcept { return binary_bound<true>(query); }
183 std::size_t lower_bound_simd(key_type const &query) const noexcept { return simd_bound<false>(query); }
184 std::size_t upper_bound_simd(key_type const &query) const noexcept { return simd_bound<true>(query); }
185 private:
186 std::span<std::byte const> bytes_;
187 bool prefer_simd() const noexcept {
188 // Measured lower-bound choices; explicit methods remain available for
189 // other hosts/workloads. Complete 2^m-1 scalar windows are especially cheap.
190 if constexpr (std::is_same_v<Arch, simd::neon> && std::endian::native == std::endian::little) {
191 if constexpr (Words == 4)
192 return (size() >= 2 && size() <= 4) || size() == 15 || size() == 16 || size() == 32;
193 else return size() == 4;
194 } else if constexpr (std::is_same_v<Arch, simd::avx2> && std::endian::native == std::endian::little) {
195 if constexpr (Words == 1) return size() == 4;
196 else if constexpr (Words == 2) return size() >= 2 && size() <= 4;
197 else return size() == 2 || size() == 4;
198 } else return false;
199 }
200 template <bool Upper> std::size_t binary_bound(key_type const &query) const noexcept {
201 auto count = size();
202 std::size_t first = 0;
203 auto step = std::bit_floor(count);
204 if (std::has_single_bit(count + 1)) {
205 // Exactly log2(count+1) comparisons for a complete 2^m-1 window.
206 for (; step; step >>= 1)
207 first += step * fixed_search_detail::precedes<Words, Upper>(
208 bytes_.data() + (first + step - 1) * key_bytes, query);
209 } else {
210 for (; step; step >>= 1) {
211 auto next = first + step;
212 auto probe = std::min(next, count) - 1;
213 auto below = fixed_search_detail::precedes<Words, Upper>(bytes_.data() + probe * key_bytes, query);
214 first += step * ((next <= count) & below);
215 }
216 }
217 return first;
218 }
219 template <bool Upper> simd_inline void pivot_step(key_type const &query, std::size_t &first, std::size_t &count) const noexcept {
220 // Called only with at least three keys, giving three distinct pivots.
221 std::array<std::size_t, 3> positions{count >> 2, count >> 1, (3 * count) >> 2};
222 unsigned rank;
223 if constexpr (Words == 4 && std::is_same_v<Arch, simd::neon> && std::endian::native == std::endian::little) {
224 auto queries = simd::vec<std::uint32_t, 4, Arch>::load(query.data());
225 rank = 0;
226 for (unsigned i = 0; i < 3; ++i)
227 rank += fixed_search_detail::precedes_four<Upper, Arch>(bytes_.data() + (first + positions[i]) * key_bytes, queries);
228 } else
229 {
230 std::array<std::byte, 3 * key_bytes> pivots;
231 for (unsigned i = 0; i < 3; ++i)
232 std::memcpy(pivots.data() + i * key_bytes, bytes_.data() + (first + positions[i]) * key_bytes, key_bytes);
233 rank = fixed_search_detail::population<Words, Upper, Arch>(pivots.data(), 3, query);
234 }
235 auto start = ((rank * count) >> 2) + (rank != 0);
236 auto end = ((rank + 1) * count) >> 2;
237 first += start;
238 count = end - start;
239 }
240 template <bool Upper, unsigned Bucket> simd_inline std::size_t small_bound(key_type const &query) const noexcept {
241 std::size_t first = 0, count = size();
242 if constexpr (Bucket > 4) pivot_step<Upper>(query, first, count);
243 // Original sizes 17..32 leave 3..8 keys after the first pivot step.
244 if constexpr (Bucket > 16) pivot_step<Upper>(query, first, count);
245 if (!count) return first;
246 return first + fixed_search_detail::population<Words, Upper, Arch>(bytes_.data() + first * key_bytes, unsigned(count), query);
247 }
248 template <bool Upper> simd_inline std::size_t simd_bound(key_type const &query) const noexcept {
249 if (size() <= 4) return small_bound<Upper, 4>(query);
250 if (size() <= 8) return small_bound<Upper, 8>(query);
251 if (size() <= 16) return small_bound<Upper, 16>(query);
252 if (size() <= explicit_simd_limit) return small_bound<Upper, 32>(query);
253 return binary_bound<Upper>(query);
254 }
255 };
256}
Shares explicit SIMD architecture traits with Everett's kernels.
Outlines exceptional check failures while preserving their types and messages.
unsigned population(std::byte const *source, unsigned count, std::array< std::uint32_t, Words > const &query) noexcept
Definition fixed_search.h:77
std::uint32_t load_word(std::byte const *source) noexcept
Definition fixed_search.h:31
bool precedes_four(std::byte const *source, simd::vec< std::uint32_t, 4, Arch > query) noexcept
Definition fixed_search.h:61
std::array< std::uint32_t, Lanes > repeated(std::array< std::uint32_t, Words > const &query) noexcept
Definition fixed_search.h:42
bool precedes(std::byte const *source, std::array< std::uint32_t, Words > const &query) noexcept
Definition fixed_search.h:49
Definition active_engine.h:18
Definition fixed_search.h:149
std::size_t small_bound(key_type const &query) const noexcept
Definition fixed_search.h:240
std::span< std::byte const > bytes_
Definition fixed_search.h:186
std::size_t upper_bound_binary(key_type const &query) const noexcept
Definition fixed_search.h:182
Arch architecture
Definition fixed_search.h:151
bool empty() const noexcept
Definition fixed_search.h:161
std::size_t upper_bound_simd(key_type const &query) const noexcept
Definition fixed_search.h:184
std::size_t lower_bound_simd(key_type const &query) const noexcept
Definition fixed_search.h:183
std::size_t upper_bound(key_type const &query) const noexcept
Definition fixed_search.h:178
static constexpr std::size_t explicit_simd_limit
Definition fixed_search.h:154
bool prefer_simd() const noexcept
Definition fixed_search.h:187
void pivot_step(key_type const &query, std::size_t &first, std::size_t &count) const noexcept
Definition fixed_search.h:219
fixed_key_view subview(std::size_t first, std::size_t count) const
Definition fixed_search.h:170
key_type key_at(std::size_t ordinal) const
Definition fixed_search.h:163
std::size_t lower_bound_binary(key_type const &query) const noexcept
Definition fixed_search.h:181
static constexpr std::size_t key_bytes
Definition fixed_search.h:153
fixed_key_view(std::span< std::byte const > bytes)
Definition fixed_search.h:156
std::size_t binary_bound(key_type const &query) const noexcept
Definition fixed_search.h:200
std::size_t simd_bound(key_type const &query) const noexcept
Definition fixed_search.h:248
std::array< std::uint32_t, Words > key_type
Definition fixed_search.h:152
std::size_t lower_bound(key_type const &query) const noexcept
Definition fixed_search.h:175
std::size_t size() const noexcept
Definition fixed_search.h:160
std::span< std::byte const > bytes() const noexcept
Definition fixed_search.h:162