simdjson 5.0.2
Ridiculously Fast JSON
Loading...
Searching...
No Matches
key_selector.h
1#ifndef SIMDJSON_GENERIC_ONDEMAND_KEY_SELECTOR_H
2#define SIMDJSON_GENERIC_ONDEMAND_KEY_SELECTOR_H
3
4#ifndef SIMDJSON_CONDITIONAL_INCLUDE
5#include "simdjson/base.h"
6#include "simdjson/common_defs.h"
7#include "simdjson/constevalutil.h"
8#include "simdjson/generic/ondemand/raw_json_string.h"
9#endif // SIMDJSON_CONDITIONAL_INCLUDE
10
11#include <array>
12#include <string> // std::string (key_selector::describe)
13#include <string_view>
14#include <cstddef>
15#include <cstdint>
16#include <cstring> // std::memcpy (portable unaligned window load)
17#include <utility> // std::index_sequence (window candidate dispatch)
18#include <type_traits> // std::integral_constant (window candidate dispatch)
19
20// AArch64 only: 32-bit ARM (ARMv7) defines __ARM_NEON too, but lacks the
21// A64-only horizontal reductions (vmaxvq_u32) used below. See issue #2885.
22#if defined(__aarch64__)
23 #include <arm_neon.h>
24 #define SIMDJSON_KEY_SELECTOR_HAS_NEON 1
25#else
26 #define SIMDJSON_KEY_SELECTOR_HAS_NEON 0
27#endif
28#if defined(__SSE2__)
29 #include <emmintrin.h>
30 #define SIMDJSON_KEY_SELECTOR_HAS_SSE2 1
31#else
32 #define SIMDJSON_KEY_SELECTOR_HAS_SSE2 0
33#endif
34#if defined(__loongarch_sx)
35 #include <lsxintrin.h>
36 #define SIMDJSON_KEY_SELECTOR_HAS_LSX 1
37#else
38 #define SIMDJSON_KEY_SELECTOR_HAS_LSX 0
39#endif
40
41#if SIMDJSON_SUPPORTS_CONCEPTS
42
43namespace simdjson {
44namespace SIMDJSON_IMPLEMENTATION {
45namespace ondemand {
46
47namespace key_selector_detail {
48
49// ============================================================================
50// Compile-time perfect-hash generator.
51// It scales to ~100 keys at compile time by determining association values one (position, character)
52// symbol at a time (gperf-style) instead of an exhaustive offset search, and
53// falls back to a Hash-and-Displace construction for large/awkward key sets.
54//
55// Only flat tables survive to runtime; the lookup is a few additions plus a
56// single SIMD key comparison (see match_raw below).
57// ============================================================================
58
59// Maximum number of character positions the gperf hash may combine.
60static constexpr std::size_t MAX_POSITIONS = 16;
61// Sentinel "position" meaning "the last character of the key".
62static constexpr std::size_t LAST_CHAR = std::size_t(-1);
63// Runtime-encoded sentinels (stored in uint8 tables).
64static constexpr std::uint8_t POS_LAST_CHAR = 0xFF; // positions_[i] == last char
65static constexpr std::uint8_t HD_MODE = 0xFF; // num_positions == H&D mode
66// Flags stored in positions[2] in H&D mode to select the key-hash variant.
67static constexpr std::size_t HD_HASH_2BYTE_FLAG = 2;
68static constexpr std::size_t HD_HASH_4BYTE_FLAG = 4;
69
70constexpr std::size_t next_power_of_2(std::size_t n) noexcept {
71 if (n == 0) { return 1; }
72 std::size_t p = 1;
73 while (p < n) { p <<= 1; }
74 return p;
75}
76
77// Character at a given position (LAST_CHAR means last character), or 256 if out
78// of bounds.
79constexpr std::size_t char_at(std::string_view key, std::size_t pos) noexcept {
80 if (pos == LAST_CHAR) {
81 if (key.empty()) { return 256; }
82 return static_cast<unsigned char>(key[key.size() - 1]);
83 }
84 if (pos >= key.size()) { return 256; }
85 return static_cast<unsigned char>(key[pos]);
86}
87
88// Count key pairs that a set of positions fails to distinguish. Keys whose
89// lengths differ modulo the table size are separated by the length term in the
90// hash, so they need no position coverage.
91template <std::size_t N>
92consteval std::size_t count_undistinguished_pairs(
93 const std::array<std::string_view, N>& keys,
94 const std::size_t* positions,
95 std::size_t num_positions,
96 std::size_t modulus) {
97 std::size_t count = 0;
98 for (std::size_t i = 0; i < N; ++i) {
99 for (std::size_t j = i + 1; j < N; ++j) {
100 if (keys[i].size() % modulus != keys[j].size() % modulus) { continue; }
101 bool distinguished = false;
102 for (std::size_t p = 0; p < num_positions; ++p) {
103 if (char_at(keys[i], positions[p]) != char_at(keys[j], positions[p])) {
104 distinguished = true;
105 break;
106 }
107 }
108 if (!distinguished) { ++count; }
109 }
110 }
111 return count;
112}
113
114template <std::size_t N>
115consteval bool positions_distinguish(
116 const std::array<std::string_view, N>& keys,
117 const std::size_t* positions,
118 std::size_t num_positions,
119 std::size_t modulus) {
120 return count_undistinguished_pairs<N>(keys, positions, num_positions, modulus) == 0;
121}
122
123// Number of distinct (length % modulus, char_at(key, pos)) pairs at a position.
124template <std::size_t N>
125consteval std::size_t discriminating_power(
126 const std::array<std::string_view, N>& keys,
127 std::size_t pos,
128 std::size_t modulus) {
129 struct pair { std::size_t len_mod; std::size_t ch; };
130 std::array<pair, N> pairs{};
131 for (std::size_t i = 0; i < N; ++i) {
132 pairs[i] = {keys[i].size() % modulus, char_at(keys[i], pos)};
133 }
134 std::size_t count = 0;
135 for (std::size_t i = 0; i < N; ++i) {
136 bool dup = false;
137 for (std::size_t j = 0; j < i; ++j) {
138 if (pairs[i].len_mod == pairs[j].len_mod && pairs[i].ch == pairs[j].ch) {
139 dup = true;
140 break;
141 }
142 }
143 if (!dup) { ++count; }
144 }
145 return count;
146}
147
148template <std::size_t N>
149consteval std::size_t max_key_length(const std::array<std::string_view, N>& keys) {
150 std::size_t m = 0;
151 for (std::size_t i = 0; i < N; ++i) {
152 if (keys[i].size() > m) { m = keys[i].size(); }
153 }
154 return m;
155}
156
157// Bounded backtracking DFS for a minimal set of distinguishing positions.
158template <std::size_t N>
159consteval bool backtracking_search(
160 const std::array<std::string_view, N>& keys,
161 const std::size_t* candidates,
162 std::size_t num_candidates,
163 std::size_t* positions,
164 std::size_t& num_positions_out,
165 std::size_t& budget,
166 std::size_t modulus) {
167 constexpr std::size_t MAX_DEPTH = 8;
168 std::size_t breadth = num_candidates < 20 ? num_candidates : 20;
169
170 struct frame { std::size_t depth; std::size_t next_ci; std::size_t parent_count; };
171 std::array<frame, MAX_DEPTH + 1> stack{};
172 std::size_t sp = 0;
173
174 std::size_t initial_count = count_undistinguished_pairs<N>(keys, positions, 0, modulus);
175 if (budget > 0) { --budget; }
176 if (initial_count == 0) { num_positions_out = 0; return true; }
177
178 stack[0] = {0, 0, initial_count};
179
180 while (budget > 0) {
181 if (sp > MAX_DEPTH) {
182 if (sp == 0) { break; }
183 --sp;
184 ++stack[sp].next_ci;
185 continue;
186 }
187 auto& f = stack[sp];
188 if (f.next_ci >= breadth) {
189 if (sp == 0) { break; }
190 --sp;
191 ++stack[sp].next_ci;
192 continue;
193 }
194 positions[sp] = candidates[f.next_ci];
195 --budget;
196 std::size_t new_count = count_undistinguished_pairs<N>(keys, positions, sp + 1, modulus);
197 if (new_count == 0) { num_positions_out = sp + 1; return true; }
198 if (new_count < f.parent_count && sp + 1 < MAX_DEPTH) {
199 stack[sp + 1] = {sp + 1, f.next_ci + 1, new_count};
200 ++sp;
201 } else {
202 ++f.next_ci;
203 }
204 }
205 return false;
206}
207
208// Phase 1: select character positions that distinguish all colliding pairs.
209template <std::size_t N>
210consteval std::size_t select_positions(
211 const std::array<std::string_view, N>& keys,
212 std::array<std::size_t, MAX_POSITIONS>& positions,
213 std::size_t modulus) {
214 if (positions_distinguish<N>(keys, positions.data(), 0, modulus)) { return 0; }
215
216 std::size_t max_len = max_key_length(keys);
217 constexpr std::size_t MAX_CANDIDATES = 256;
218 std::array<std::size_t, MAX_CANDIDATES> candidates{};
219 std::array<std::size_t, MAX_CANDIDATES> powers{};
220 std::size_t num_candidates = 0;
221 for (std::size_t p = 0; p < max_len && num_candidates < MAX_CANDIDATES - 1; ++p) {
222 candidates[num_candidates] = p;
223 powers[num_candidates] = discriminating_power(keys, p, modulus);
224 ++num_candidates;
225 }
226 if (num_candidates < MAX_CANDIDATES) {
227 candidates[num_candidates] = LAST_CHAR;
228 powers[num_candidates] = discriminating_power(keys, LAST_CHAR, modulus);
229 ++num_candidates;
230 }
231 for (std::size_t i = 0; i < num_candidates; ++i) {
232 for (std::size_t j = i + 1; j < num_candidates; ++j) {
233 if (powers[j] > powers[i]) {
234 auto tc = candidates[i]; candidates[i] = candidates[j]; candidates[j] = tc;
235 auto tp = powers[i]; powers[i] = powers[j]; powers[j] = tp;
236 }
237 }
238 }
239
240 positions[0] = candidates[0];
241 if (positions_distinguish<N>(keys, positions.data(), 1, modulus)) { return 1; }
242
243 positions[0] = 0;
244 positions[1] = LAST_CHAR;
245 if (positions_distinguish<N>(keys, positions.data(), 2, modulus)) { return 2; }
246
247 {
248 std::size_t budget = 5000;
249 std::size_t num_found = 0;
250 if (backtracking_search<N>(keys, candidates.data(), num_candidates,
251 positions.data(), num_found, budget, modulus)) {
252 return num_found;
253 }
254 }
255
256 std::size_t num_pos = 0;
257 for (std::size_t ci = 0; ci < num_candidates && num_pos < MAX_POSITIONS; ++ci) {
258 bool already = false;
259 for (std::size_t p = 0; p < num_pos; ++p) {
260 if (positions[p] == candidates[ci]) { already = true; break; }
261 }
262 if (already) { continue; }
263 positions[num_pos] = candidates[ci];
264 ++num_pos;
265 if (positions_distinguish<N>(keys, positions.data(), num_pos, modulus)) { return num_pos; }
266 }
267
268 throw "Failed to find distinguishing positions for perfect hash";
269}
270
271// Result of PHF computation. A max-sized slot_to_key array lets the same struct
272// type carry any chosen table size.
273template <std::size_t N>
274struct phf_result {
275 // Allow up to 8x the minimum table size. Sparser tables solve faster.
276 static constexpr std::size_t MAX_TABLE_SIZE = next_power_of_2(N) * 8;
277 std::size_t table_size{};
278 std::array<std::array<std::size_t, 256>, MAX_POSITIONS> asso_values{};
279 std::size_t num_positions{};
280 std::array<std::size_t, MAX_POSITIONS> positions{};
281 std::array<std::size_t, MAX_TABLE_SIZE> slot_to_key{};
282};
283
284// Partition-based asso_values search (gperf-style). Determines asso_values one
285// (position, character) symbol at a time; never revisits a value. Equivalence
286// classes (keys sharing the same undetermined symbols) keep the search cheap.
287template <std::size_t N, std::size_t M>
288consteval bool try_generate_gperf(
289 const std::array<std::string_view, N>& keys,
290 std::array<std::array<std::size_t, 256>, MAX_POSITIONS>& asso_values,
291 std::size_t& num_positions,
292 std::array<std::size_t, MAX_POSITIONS>& positions,
293 std::array<std::size_t, M>& slot_to_key) {
294 num_positions = select_positions<N>(keys, positions, M);
295
296 for (std::size_t p = 0; p < MAX_POSITIONS; ++p) {
297 for (std::size_t c = 0; c < 256; ++c) { asso_values[p][c] = 0; }
298 }
299
300 if (num_positions == 0) {
301 for (std::size_t i = 0; i < M; ++i) { slot_to_key[i] = N; }
302 for (std::size_t i = 0; i < N; ++i) {
303 std::size_t slot = keys[i].size() % M;
304 if (slot_to_key[slot] != N) { return false; }
305 slot_to_key[slot] = i;
306 }
307 return true;
308 }
309
310 std::array<std::array<std::size_t, MAX_POSITIONS>, N> kchars{};
311 for (std::size_t k = 0; k < N; ++k) {
312 for (std::size_t p = 0; p < num_positions; ++p) {
313 kchars[k][p] = char_at(keys[k], positions[p]);
314 }
315 }
316
317 struct sym_t { std::size_t pos; std::size_t ch; std::size_t freq; };
318 constexpr std::size_t MAX_SYMS = MAX_POSITIONS * 256;
319 std::array<sym_t, MAX_SYMS> syms{};
320 std::size_t nsyms = 0;
321 for (std::size_t p = 0; p < num_positions; ++p) {
322 std::array<std::size_t, 256> freq{};
323 for (std::size_t k = 0; k < N; ++k) {
324 std::size_t c = kchars[k][p];
325 if (c < 256) { freq[c]++; }
326 }
327 for (std::size_t c = 0; c < 256; ++c) {
328 if (freq[c] > 0) { syms[nsyms++] = {p, c, freq[c]}; }
329 }
330 }
331 for (std::size_t i = 0; i < nsyms; ++i) {
332 for (std::size_t j = i + 1; j < nsyms; ++j) {
333 if (syms[j].freq > syms[i].freq) {
334 auto tmp = syms[i]; syms[i] = syms[j]; syms[j] = tmp;
335 }
336 }
337 }
338
339 std::array<std::size_t, N> phash{};
340 for (std::size_t k = 0; k < N; ++k) { phash[k] = keys[k].size(); }
341
342 std::array<std::array<uint64_t, 256>, MAX_POSITIONS> salt{};
343 {
344 uint64_t s = 0x9e3779b97f4a7c15ULL;
345 for (std::size_t p = 0; p < num_positions; ++p) {
346 for (std::size_t c = 0; c < 256; ++c) {
347 s = s * 6364136223846793005ULL + 1442695040888963407ULL;
348 salt[p][c] = s;
349 }
350 }
351 }
352 std::array<uint64_t, N> sig{};
353 for (std::size_t k = 0; k < N; ++k) {
354 uint64_t s = 0;
355 for (std::size_t p = 0; p < num_positions; ++p) {
356 std::size_t c = kchars[k][p];
357 if (c < 256) { s ^= salt[p][c]; }
358 }
359 sig[k] = s;
360 }
361 std::array<std::size_t, N> order{};
362 for (std::size_t k = 0; k < N; ++k) { order[k] = k; }
363
364 std::array<std::size_t, M> slot_gen{};
365 std::size_t gen = 0;
366
367 std::size_t search_limit = next_power_of_2(M);
368 if (search_limit < 32) { search_limit = 32; }
369
370 for (std::size_t si = 0; si < nsyms; ++si) {
371 std::size_t sp = syms[si].pos;
372 std::size_t sc = syms[si].ch;
373
374 uint64_t sp_salt = salt[sp][sc];
375 for (std::size_t k = 0; k < N; ++k) {
376 if (kchars[k][sp] == sc) { sig[k] ^= sp_salt; }
377 }
378
379 for (std::size_t i = 1; i < N; ++i) {
380 std::size_t x = order[i];
381 uint64_t xs = sig[x];
382 std::size_t j = i;
383 while (j > 0 && sig[order[j - 1]] > xs) {
384 order[j] = order[j - 1];
385 --j;
386 }
387 order[j] = x;
388 }
389
390 bool found = false;
391 for (std::size_t v = 0; v < search_limit && !found; ++v) {
392 bool collision = false;
393 std::size_t ci = 0;
394 while (ci < N && !collision) {
395 uint64_t class_sig = sig[order[ci]];
396 std::size_t cj = ci;
397 while (cj < N && sig[order[cj]] == class_sig) { ++cj; }
398 if (cj - ci > 1) {
399 ++gen;
400 for (std::size_t x = ci; x < cj; ++x) {
401 std::size_t k = order[x];
402 std::size_t h = phash[k];
403 if (kchars[k][sp] == sc) { h += v; }
404 h %= M;
405 if (slot_gen[h] == gen) { collision = true; break; }
406 slot_gen[h] = gen;
407 }
408 }
409 ci = cj;
410 }
411 if (!collision) {
412 asso_values[sp][sc] = v;
413 for (std::size_t k = 0; k < N; ++k) {
414 if (kchars[k][sp] == sc) { phash[k] += v; }
415 }
416 found = true;
417 }
418 }
419 if (!found) { return false; }
420 }
421
422 for (std::size_t i = 0; i < M; ++i) { slot_to_key[i] = N; }
423 for (std::size_t i = 0; i < N; ++i) {
424 std::size_t slot = phash[i] % M;
425 if (slot_to_key[slot] != N) { return false; }
426 slot_to_key[slot] = i;
427 }
428 std::size_t filled = 0;
429 for (std::size_t i = 0; i < M; ++i) {
430 if (slot_to_key[i] != N) { ++filled; }
431 }
432 return filled == N;
433}
434
435template <std::size_t N, std::size_t M>
436consteval bool try_compute_phf(const std::array<std::string_view, N>& keys, phf_result<N>& result) {
437 static_assert(M <= phf_result<N>::MAX_TABLE_SIZE, "Table size M exceeds maximum");
438 std::array<std::array<std::size_t, 256>, MAX_POSITIONS> asso{};
439 std::size_t npos{};
440 std::array<std::size_t, MAX_POSITIONS> pos{};
441 std::array<std::size_t, M> s2k{};
442 if (try_generate_gperf<N, M>(keys, asso, npos, pos, s2k)) {
443 result.table_size = M;
444 result.asso_values = asso;
445 result.num_positions = npos;
446 result.positions = pos;
447 for (std::size_t i = 0; i < M; ++i) { result.slot_to_key[i] = s2k[i]; }
448 for (std::size_t i = M; i < phf_result<N>::MAX_TABLE_SIZE; ++i) { result.slot_to_key[i] = N; }
449 return true;
450 }
451 return false;
452}
453
454template <std::size_t N, std::size_t M, std::size_t MaxM>
455consteval bool try_gperf_po2(const std::array<std::string_view, N>& keys, phf_result<N>& result) {
456 if (try_compute_phf<N, M>(keys, result)) { return true; }
457 constexpr std::size_t NextM = M * 2;
458 if constexpr (NextM <= MaxM) { return try_gperf_po2<N, NextM, MaxM>(keys, result); }
459 return false;
460}
461
462// --- Hash-and-Displace fallback --------------------------------------------
463
464constexpr std::size_t hd_bucket_hash(std::string_view key) noexcept {
465 std::size_t c0 = key.empty() ? 0 : static_cast<unsigned char>(key[0]);
466 std::size_t c1 = key.empty() ? 0 : static_cast<unsigned char>(key[key.size() - 1]);
467 return (c0 + c1 * 3 + key.size() * 17) & 0xFF;
468}
469constexpr std::size_t hd_safe_char(const char* p, std::size_t len, std::size_t idx) noexcept {
470 std::size_t has = static_cast<std::size_t>(idx < len);
471 std::size_t si = idx & (std::size_t{0} - has);
472 return static_cast<unsigned char>(p[si]) & (std::size_t{0} - has);
473}
474constexpr std::size_t hd_key_hash_2(std::string_view key) noexcept {
475 std::size_t kc = key.size();
476 kc = kc * 31 + static_cast<unsigned char>(key[0]);
477 kc = kc * 31 + hd_safe_char(key.data(), key.size(), 1);
478 return kc;
479}
480constexpr std::size_t hd_key_hash_4(std::string_view key) noexcept {
481 std::size_t kc = key.size();
482 kc = kc * 31 + static_cast<unsigned char>(key[0]);
483 kc = kc * 31 + hd_safe_char(key.data(), key.size(), 1);
484 kc = kc * 31 + hd_safe_char(key.data(), key.size(), 2);
485 kc = kc * 31 + hd_safe_char(key.data(), key.size(), 3);
486 return kc;
487}
488
489template <std::size_t N, std::size_t M>
490consteval bool try_hash_and_displace(
491 const std::array<std::string_view, N>& keys,
492 std::array<std::array<std::size_t, 256>, MAX_POSITIONS>& asso_values,
493 std::size_t& num_positions,
494 std::array<std::size_t, MAX_POSITIONS>& positions,
495 std::array<std::size_t, M>& slot_to_key) {
496 num_positions = select_positions<N>(keys, positions, M);
497
498 if (num_positions == 0) {
499 for (std::size_t i = 0; i < 256; ++i) { asso_values[0][i] = 0; }
500 for (std::size_t i = 0; i < M; ++i) { slot_to_key[i] = N; }
501 for (std::size_t i = 0; i < N; ++i) {
502 std::size_t slot = keys[i].size() % M;
503 if (slot_to_key[slot] != N) { return false; }
504 slot_to_key[slot] = i;
505 }
506 return true;
507 }
508
509 for (std::size_t i = 0; i < 256; ++i) { asso_values[0][i] = 0; }
510 num_positions = HD_MODE; // sentinel for H&D mode
511 positions[0] = 0;
512 positions[1] = LAST_CHAR;
513
514 std::array<std::size_t, N> key_bucket{};
515 for (std::size_t i = 0; i < N; ++i) { key_bucket[i] = hd_bucket_hash(keys[i]); }
516
517 struct bucket_info { std::size_t ch; std::size_t count; };
518 std::array<bucket_info, N> buckets{};
519 std::size_t num_buckets = 0;
520 for (std::size_t i = 0; i < N; ++i) {
521 std::size_t bk = key_bucket[i];
522 bool found = false;
523 for (std::size_t b = 0; b < num_buckets; ++b) {
524 if (buckets[b].ch == bk) { ++buckets[b].count; found = true; break; }
525 }
526 if (!found) { buckets[num_buckets++] = {bk, 1}; }
527 }
528 for (std::size_t i = 0; i < num_buckets; ++i) {
529 for (std::size_t j = i + 1; j < num_buckets; ++j) {
530 if (buckets[j].count > buckets[i].count) {
531 auto tmp = buckets[i]; buckets[i] = buckets[j]; buckets[j] = tmp;
532 }
533 }
534 }
535
536 auto try_placement = [&](auto key_hash_fn) -> bool {
537 for (std::size_t i = 0; i < M; ++i) { slot_to_key[i] = N; }
538 for (std::size_t i = 0; i < 256; ++i) { asso_values[0][i] = 0; }
539 for (std::size_t b = 0; b < num_buckets; ++b) {
540 std::size_t ch = buckets[b].ch;
541 std::array<std::size_t, N> bucket_keys{};
542 std::size_t bk_count = 0;
543 for (std::size_t i = 0; i < N; ++i) {
544 if (key_bucket[i] == ch) { bucket_keys[bk_count++] = i; }
545 }
546 bool placed = false;
547 std::size_t max_d = M < 255 ? M : 255;
548 for (std::size_t d = 0; d < max_d; ++d) {
549 bool ok = true;
550 std::array<std::size_t, N> bucket_slots{};
551 for (std::size_t k = 0; k < bk_count; ++k) {
552 std::size_t slot = (d + key_hash_fn(keys[bucket_keys[k]])) % M;
553 if (slot_to_key[slot] != N) { ok = false; break; }
554 for (std::size_t k2 = 0; k2 < k; ++k2) {
555 if (bucket_slots[k2] == slot) { ok = false; break; }
556 }
557 if (!ok) { break; }
558 bucket_slots[k] = slot;
559 }
560 if (ok) {
561 asso_values[0][ch] = d;
562 for (std::size_t k = 0; k < bk_count; ++k) {
563 slot_to_key[bucket_slots[k]] = bucket_keys[k];
564 }
565 placed = true;
566 break;
567 }
568 }
569 if (!placed) { return false; }
570 }
571 std::size_t filled = 0;
572 for (std::size_t i = 0; i < M; ++i) {
573 if (slot_to_key[i] != N) { ++filled; }
574 }
575 return filled == N;
576 };
577
578 if (try_placement([](std::string_view k) { return hd_key_hash_2(k); })) {
579 positions[2] = HD_HASH_2BYTE_FLAG;
580 return true;
581 }
582 if (try_placement([](std::string_view k) { return hd_key_hash_4(k); })) {
583 positions[2] = HD_HASH_4BYTE_FLAG;
584 return true;
585 }
586 return false;
587}
588
589template <std::size_t N, std::size_t M>
590consteval bool try_compute_phf_hd(const std::array<std::string_view, N>& keys, phf_result<N>& result) {
591 static_assert(M <= phf_result<N>::MAX_TABLE_SIZE, "Table size M exceeds maximum");
592 std::array<std::array<std::size_t, 256>, MAX_POSITIONS> asso{};
593 std::size_t npos{};
594 std::array<std::size_t, MAX_POSITIONS> pos{};
595 std::array<std::size_t, M> s2k{};
596 if (try_hash_and_displace<N, M>(keys, asso, npos, pos, s2k)) {
597 result.table_size = M;
598 result.asso_values = asso;
599 result.num_positions = npos;
600 result.positions = pos;
601 for (std::size_t i = 0; i < M; ++i) { result.slot_to_key[i] = s2k[i]; }
602 for (std::size_t i = M; i < phf_result<N>::MAX_TABLE_SIZE; ++i) { result.slot_to_key[i] = N; }
603 return true;
604 }
605 return false;
606}
607
608template <std::size_t N, std::size_t M>
609consteval phf_result<N> compute_phf_hd_po2(const std::array<std::string_view, N>& keys) {
610 static_assert(M <= phf_result<N>::MAX_TABLE_SIZE, "Table size M exceeds maximum");
611 phf_result<N> result{};
612 if (try_compute_phf_hd<N, M>(keys, result)) { return result; }
613 constexpr std::size_t NextM = M * 2;
614 if constexpr (NextM <= phf_result<N>::MAX_TABLE_SIZE) {
615 return compute_phf_hd_po2<N, NextM>(keys);
616 } else {
617 throw "Hash-and-Displace: failed to find valid table size";
618 }
619}
620
621// Compute a perfect hash for `keys`: try gperf at power-of-two sizes (capped so
622// the runtime tables stay within uint8 indices), then fall back to H&D.
623template <std::size_t N>
624consteval phf_result<N> compute_phf(const std::array<std::string_view, N>& keys) {
625 constexpr std::size_t StartM = next_power_of_2(N);
626 constexpr std::size_t GPERF_MAX_TABLE =
627 phf_result<N>::MAX_TABLE_SIZE < 256 ? phf_result<N>::MAX_TABLE_SIZE : 256;
628 if constexpr (StartM <= GPERF_MAX_TABLE) {
629 phf_result<N> result{};
630 if (try_gperf_po2<N, StartM, GPERF_MAX_TABLE>(keys, result)) { return result; }
631 }
632 return compute_phf_hd_po2<N, StartM>(keys);
633}
634
635// ============================================================================
636// Runtime tables (flat, uint8) derived from a phf_result.
637// ============================================================================
638
639template <std::size_t N, std::size_t TableSize, std::size_t MaxKeyLen>
640struct phf_data {
641 std::array<std::array<std::uint8_t, 256>, MAX_POSITIONS> asso_values{};
642 std::array<std::uint8_t, MAX_POSITIONS> positions{};
643 std::uint8_t num_positions{};
644 std::uint8_t hd_hash_variant{}; // 2 or 4 (H&D only)
645 std::array<std::uint8_t, TableSize> slot_to_key{};
646 // slot_key_bytes[s] holds the key stored at slot s, zero-padded to a 16-byte
647 // multiple so the SIMD comparison can read a whole register.
648 std::array<std::array<char, ((MaxKeyLen + 15) / 16) * 16>, TableSize> slot_key_bytes{};
649 std::array<std::uint8_t, TableSize> slot_key_len{};
650};
651
652template <std::size_t N>
653constexpr std::size_t compute_max_key_len(const std::array<std::string_view, N>& keys) noexcept {
654 std::size_t m = 0;
655 for (std::size_t i = 0; i < N; ++i) { if (keys[i].size() > m) { m = keys[i].size(); } }
656 return m;
657}
658
659// Validate keys and build the runtime tables from the computed perfect hash.
660template <std::size_t N, std::size_t TableSize, std::size_t MaxKeyLen>
661consteval phf_data<N, TableSize, MaxKeyLen>
662build_phf_data(const std::array<std::string_view, N>& keys, const phf_result<N>& result) {
663 for (std::size_t i = 0; i < N; ++i) {
664 if (keys[i].empty()) { throw "empty keys are not allowed in key_selector"; }
665 if (keys[i].size() > MaxKeyLen) { throw "key length exceeds MaxKeyLen"; }
666 for (char c : keys[i]) {
667 if (c == '\\') { throw "backslash not allowed in key_selector keys"; }
668 if (c == '"') { throw "quote not allowed in key_selector keys"; }
669 if (c == '\0') { throw "null byte not allowed in key_selector keys"; }
670 }
671 for (std::size_t j = i + 1; j < N; ++j) {
672 if (keys[i] == keys[j]) { throw "duplicate keys in key_selector"; }
673 }
674 }
675
676 phf_data<N, TableSize, MaxKeyLen> out{};
677
678 if (result.num_positions == HD_MODE) {
679 // H&D mode: single displacement table in asso_values[0].
680 for (std::size_t c = 0; c < 256; ++c) {
681 out.asso_values[0][c] = static_cast<std::uint8_t>(result.asso_values[0][c]);
682 }
683 out.num_positions = static_cast<std::uint8_t>(HD_MODE);
684 out.hd_hash_variant = static_cast<std::uint8_t>(result.positions[2]);
685 } else {
686 for (std::size_t pi = 0; pi < result.num_positions; ++pi) {
687 for (std::size_t c = 0; c < 256; ++c) {
688 out.asso_values[pi][c] = static_cast<std::uint8_t>(result.asso_values[pi][c] % TableSize);
689 }
690 }
691 out.num_positions = static_cast<std::uint8_t>(result.num_positions);
692 for (std::size_t i = 0; i < result.num_positions; ++i) {
693 out.positions[i] = (result.positions[i] == LAST_CHAR)
694 ? POS_LAST_CHAR
695 : static_cast<std::uint8_t>(result.positions[i]);
696 }
697 }
698
699 for (std::size_t s = 0; s < TableSize; ++s) {
700 out.slot_to_key[s] = static_cast<std::uint8_t>(result.slot_to_key[s]);
701 }
702
703 for (std::size_t s = 0; s < TableSize; ++s) {
704 std::size_t ki = result.slot_to_key[s];
705 if (ki < N) {
706 auto k = keys[ki];
707 out.slot_key_len[s] = static_cast<std::uint8_t>(k.size());
708 for (std::size_t c = 0; c < k.size(); ++c) { out.slot_key_bytes[s][c] = k[c]; }
709 } else {
710 out.slot_key_len[s] = 0; // empty slot: no length can match
711 }
712 }
713 return out;
714}
715
716// --- SIMD runtime primitives ------------------------------------------------
717
718// The runtime matchers read whole 16-byte blocks up to offset 63 (four blocks)
719// for keys as long as the 63-character maximum. That read must stay within the
720// buffer's trailing padding, so the padding has to exceed the largest offset we
721// touch.
722static_assert(SIMDJSON_PADDING > 63,
723 "key_selector requires SIMDJSON_PADDING > 63 for its SIMD key reads");
724
725#if SIMDJSON_KEY_SELECTOR_HAS_NEON
726// True when every byte of `diff` is zero. A 32-bit-lane horizontal max is
727// enough (zero iff every 32-bit word is zero) and is cheaper than a byte-wide
728// reduction. Do not replace this with a floating-point compare against 0.0:
729// flush-to-zero would treat a denormal as zero.
730simdjson_really_inline bool neon_all_bytes_zero(uint8x16_t diff) noexcept {
731 return vmaxvq_u32(vreinterpretq_u32_u8(diff)) == 0;
732}
733#endif
734
735// Scan for the terminating '"' starting at p. Returns its byte offset (= key
736// length). Caller guarantees SIMDJSON_PADDING (== 64) bytes past the JSON buffer,
737// so reading whole 16-byte blocks up to offset 63 is always safe.
738template <std::size_t MaxKeyLen>
739simdjson_really_inline std::size_t scan_key_length(const char* p) noexcept {
740 // The closing quote of the longest key sits at most at offset MaxKeyLen, so we
741 // scan as many 16-byte blocks as it takes to cover offsets 0..MaxKeyLen. The
742 // cap (63) keeps every covered offset within the 64-byte padding guarantee, so
743 // the SIMD and scalar builds agree.
744 static_assert(MaxKeyLen <= 63, "MaxKeyLen must be <= 63 for current SIMD implementations");
745 // [[maybe_unused]]: the scalar fallback (no NEON/SSE2/LSX) does not use it.
746 [[maybe_unused]] constexpr std::size_t num_blocks = MaxKeyLen / 16 + 1;
747#if SIMDJSON_KEY_SELECTOR_HAS_NEON
748 for (std::size_t b = 0; b < num_blocks; ++b) {
749 uint8x16_t v = vld1q_u8(reinterpret_cast<const uint8_t*>(p) + b * 16);
750 uint8x16_t cmp = vceqq_u8(v, vdupq_n_u8('"'));
751 uint64_t m = vget_lane_u64(
752 vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(cmp), 4)), 0);
753 if (simdjson_likely(m != 0)) { return b * 16 + (std::size_t(__builtin_ctzll(m)) >> 2); }
754 }
755 return MaxKeyLen + 1;
756#elif SIMDJSON_KEY_SELECTOR_HAS_SSE2
757 for (std::size_t b = 0; b < num_blocks; ++b) {
758 __m128i v = _mm_loadu_si128(reinterpret_cast<const __m128i*>(p + b * 16));
759 __m128i cmp = _mm_cmpeq_epi8(v, _mm_set1_epi8('"'));
760 unsigned m = static_cast<unsigned>(_mm_movemask_epi8(cmp));
761 if (simdjson_likely(m != 0)) { return b * 16 + std::size_t(__builtin_ctz(m)); }
762 }
763 return MaxKeyLen + 1;
764#elif SIMDJSON_KEY_SELECTOR_HAS_LSX
765 for (std::size_t b = 0; b < num_blocks; ++b) {
766 __m128i v = __lsx_vld(reinterpret_cast<const void*>(p + b * 16), 0);
767 __m128i cmp = __lsx_vseq_b(v, __lsx_vreplgr2vr_b('"'));
768 // vmskltz_b gathers the per-byte sign bits (set where the byte equals '"')
769 // into the low 16 bits of lane 0, the LSX equivalent of movemask.
770 unsigned m = static_cast<unsigned>(__lsx_vpickve2gr_w(__lsx_vmskltz_b(cmp), 0)) & 0xFFFFu;
771 if (simdjson_likely(m != 0)) { return b * 16 + std::size_t(__builtin_ctz(m)); }
772 }
773 return MaxKeyLen + 1;
774#else
775 for (std::size_t i = 0; i <= MaxKeyLen; ++i)
776 if (p[i] == '"') return i;
777 return MaxKeyLen + 1;
778#endif
779}
780
781// Byte-equal of p[0..len) against stored[0..len). stored is zero-padded past
782// `len`. Input is read over 16, 32, 48, or 64 bytes (padded JSON buffer
783// guaranteed; SIMDJSON_PADDING == 64).
784template <std::size_t MaxKeyLen>
785simdjson_really_inline bool compare_key_bytes(
786 const char* p, const char* stored, std::size_t len) noexcept {
787 // [[maybe_unused]]: only the NEON/SSE2/LSX branches read these; the scalar
788 // fallback build (no SIMD) leaves them unused, which is an error under -Werror.
789 [[maybe_unused]] alignas(16) static constexpr uint8_t idx16[16] =
790 {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};
791 if constexpr (MaxKeyLen <= 16) {
792#if SIMDJSON_KEY_SELECTOR_HAS_NEON
793 uint8x16_t vp = vld1q_u8(reinterpret_cast<const uint8_t*>(p));
794 uint8x16_t vs = vld1q_u8(reinterpret_cast<const uint8_t*>(stored));
795 uint8x16_t mask = vcltq_u8(vld1q_u8(idx16), vdupq_n_u8(static_cast<uint8_t>(len)));
796 uint8x16_t diff = veorq_u8(vandq_u8(vp, mask), vs);
797 return neon_all_bytes_zero(diff);
798#elif SIMDJSON_KEY_SELECTOR_HAS_SSE2
799 __m128i vp = _mm_loadu_si128(reinterpret_cast<const __m128i*>(p));
800 __m128i vs = _mm_loadu_si128(reinterpret_cast<const __m128i*>(stored));
801 __m128i idx = _mm_load_si128(reinterpret_cast<const __m128i*>(idx16));
802 __m128i mask = _mm_cmplt_epi8(idx, _mm_set1_epi8(static_cast<char>(len)));
803 __m128i eq = _mm_cmpeq_epi8(_mm_and_si128(vp, mask), vs);
804 return _mm_movemask_epi8(eq) == 0xFFFF;
805#elif SIMDJSON_KEY_SELECTOR_HAS_LSX
806 __m128i vp = __lsx_vld(reinterpret_cast<const void*>(p), 0);
807 __m128i vs = __lsx_vld(reinterpret_cast<const void*>(stored), 0);
808 __m128i idx = __lsx_vld(reinterpret_cast<const void*>(idx16), 0);
809 __m128i mask = __lsx_vslt_b(idx, __lsx_vreplgr2vr_b(static_cast<int>(len)));
810 __m128i eq = __lsx_vseq_b(__lsx_vand_v(vp, mask), vs);
811 return (static_cast<unsigned>(__lsx_vpickve2gr_w(__lsx_vmskltz_b(eq), 0)) & 0xFFFFu) == 0xFFFFu;
812#else
813 for (std::size_t i = 0; i < len; ++i)
814 if (p[i] != stored[i]) return false;
815 return true;
816#endif
817 } else if constexpr (MaxKeyLen <= 32) {
818 [[maybe_unused]] alignas(16) static constexpr uint8_t idx32_hi[16] =
819 {16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31};
820#if SIMDJSON_KEY_SELECTOR_HAS_NEON
821 uint8x16_t vp_lo = vld1q_u8(reinterpret_cast<const uint8_t*>(p));
822 uint8x16_t vp_hi = vld1q_u8(reinterpret_cast<const uint8_t*>(p) + 16);
823 uint8x16_t vs_lo = vld1q_u8(reinterpret_cast<const uint8_t*>(stored));
824 uint8x16_t vs_hi = vld1q_u8(reinterpret_cast<const uint8_t*>(stored) + 16);
825 uint8x16_t lenv = vdupq_n_u8(static_cast<uint8_t>(len));
826 uint8x16_t m_lo = vcltq_u8(vld1q_u8(idx16), lenv);
827 uint8x16_t m_hi = vcltq_u8(vld1q_u8(idx32_hi), lenv);
828 uint8x16_t d_lo = veorq_u8(vandq_u8(vp_lo, m_lo), vs_lo);
829 uint8x16_t d_hi = veorq_u8(vandq_u8(vp_hi, m_hi), vs_hi);
830 return neon_all_bytes_zero(vorrq_u8(d_lo, d_hi));
831#elif SIMDJSON_KEY_SELECTOR_HAS_SSE2
832 __m128i vp_lo = _mm_loadu_si128(reinterpret_cast<const __m128i*>(p));
833 __m128i vp_hi = _mm_loadu_si128(reinterpret_cast<const __m128i*>(p + 16));
834 __m128i vs_lo = _mm_loadu_si128(reinterpret_cast<const __m128i*>(stored));
835 __m128i vs_hi = _mm_loadu_si128(reinterpret_cast<const __m128i*>(stored + 16));
836 __m128i lenv = _mm_set1_epi8(static_cast<char>(len));
837 __m128i m_lo = _mm_cmplt_epi8(_mm_load_si128(reinterpret_cast<const __m128i*>(idx16)), lenv);
838 __m128i m_hi = _mm_cmplt_epi8(_mm_load_si128(reinterpret_cast<const __m128i*>(idx32_hi)), lenv);
839 __m128i eq_lo = _mm_cmpeq_epi8(_mm_and_si128(vp_lo, m_lo), vs_lo);
840 __m128i eq_hi = _mm_cmpeq_epi8(_mm_and_si128(vp_hi, m_hi), vs_hi);
841 return (_mm_movemask_epi8(eq_lo) & _mm_movemask_epi8(eq_hi)) == 0xFFFF;
842#elif SIMDJSON_KEY_SELECTOR_HAS_LSX
843 __m128i lenv = __lsx_vreplgr2vr_b(static_cast<int>(len));
844 __m128i vp_lo = __lsx_vld(reinterpret_cast<const void*>(p), 0);
845 __m128i vp_hi = __lsx_vld(reinterpret_cast<const void*>(p + 16), 0);
846 __m128i vs_lo = __lsx_vld(reinterpret_cast<const void*>(stored), 0);
847 __m128i vs_hi = __lsx_vld(reinterpret_cast<const void*>(stored + 16), 0);
848 __m128i m_lo = __lsx_vslt_b(__lsx_vld(reinterpret_cast<const void*>(idx16), 0), lenv);
849 __m128i m_hi = __lsx_vslt_b(__lsx_vld(reinterpret_cast<const void*>(idx32_hi), 0), lenv);
850 __m128i eq_lo = __lsx_vseq_b(__lsx_vand_v(vp_lo, m_lo), vs_lo);
851 __m128i eq_hi = __lsx_vseq_b(__lsx_vand_v(vp_hi, m_hi), vs_hi);
852 unsigned mlo = static_cast<unsigned>(__lsx_vpickve2gr_w(__lsx_vmskltz_b(eq_lo), 0)) & 0xFFFFu;
853 unsigned mhi = static_cast<unsigned>(__lsx_vpickve2gr_w(__lsx_vmskltz_b(eq_hi), 0)) & 0xFFFFu;
854 return (mlo & mhi) == 0xFFFFu;
855#else
856 for (std::size_t i = 0; i < len; ++i)
857 if (p[i] != stored[i]) return false;
858 return true;
859#endif
860 } else if constexpr (MaxKeyLen <= 64) {
861 // 3 or 4 16-byte blocks (33..48 -> 3, 49..64 -> 4). stored is zero-padded
862 // to exactly num_blocks*16 bytes (KEY_STRIDE), so neither load overruns it.
863 // [[maybe_unused]]: the scalar fallback (no NEON/SSE2/LSX) does not use it.
864 [[maybe_unused]] constexpr std::size_t num_blocks = (MaxKeyLen + 15) / 16;
865#if SIMDJSON_KEY_SELECTOR_HAS_NEON
866 uint8x16_t base = vld1q_u8(idx16);
867 uint8x16_t lenv = vdupq_n_u8(static_cast<uint8_t>(len));
868 uint8x16_t acc = vdupq_n_u8(0);
869 for (std::size_t b = 0; b < num_blocks; ++b) {
870 uint8x16_t vp = vld1q_u8(reinterpret_cast<const uint8_t*>(p) + b * 16);
871 uint8x16_t vs = vld1q_u8(reinterpret_cast<const uint8_t*>(stored) + b * 16);
872 uint8x16_t idxv = vaddq_u8(base, vdupq_n_u8(static_cast<uint8_t>(b * 16)));
873 uint8x16_t mask = vcltq_u8(idxv, lenv);
874 acc = vorrq_u8(acc, veorq_u8(vandq_u8(vp, mask), vs));
875 }
876 return neon_all_bytes_zero(acc);
877#elif SIMDJSON_KEY_SELECTOR_HAS_SSE2
878 __m128i base = _mm_load_si128(reinterpret_cast<const __m128i*>(idx16));
879 __m128i lenv = _mm_set1_epi8(static_cast<char>(len));
880 int eq = 0xFFFF;
881 for (std::size_t b = 0; b < num_blocks; ++b) {
882 __m128i vp = _mm_loadu_si128(reinterpret_cast<const __m128i*>(p + b * 16));
883 __m128i vs = _mm_loadu_si128(reinterpret_cast<const __m128i*>(stored + b * 16));
884 __m128i idxv = _mm_add_epi8(base, _mm_set1_epi8(static_cast<char>(b * 16)));
885 __m128i mask = _mm_cmplt_epi8(idxv, lenv);
886 eq &= _mm_movemask_epi8(_mm_cmpeq_epi8(_mm_and_si128(vp, mask), vs));
887 }
888 return eq == 0xFFFF;
889#elif SIMDJSON_KEY_SELECTOR_HAS_LSX
890 __m128i base = __lsx_vld(reinterpret_cast<const void*>(idx16), 0);
891 __m128i lenv = __lsx_vreplgr2vr_b(static_cast<int>(len));
892 unsigned acc = 0xFFFFu;
893 for (std::size_t b = 0; b < num_blocks; ++b) {
894 __m128i vp = __lsx_vld(reinterpret_cast<const void*>(p + b * 16), 0);
895 __m128i vs = __lsx_vld(reinterpret_cast<const void*>(stored + b * 16), 0);
896 __m128i idxv = __lsx_vadd_b(base, __lsx_vreplgr2vr_b(static_cast<int>(b * 16)));
897 __m128i mask = __lsx_vslt_b(idxv, lenv);
898 __m128i eq = __lsx_vseq_b(__lsx_vand_v(vp, mask), vs);
899 acc &= static_cast<unsigned>(__lsx_vpickve2gr_w(__lsx_vmskltz_b(eq), 0)) & 0xFFFFu;
900 }
901 return acc == 0xFFFFu;
902#else
903 for (std::size_t i = 0; i < len; ++i)
904 if (p[i] != stored[i]) return false;
905 return true;
906#endif
907 } else {
908 for (std::size_t i = 0; i < len; ++i)
909 if (p[i] != stored[i]) return false;
910 return true;
911 }
912}
913
914// --- Single 8-bit window fast path ------------------------------------------
915//
916// Many small key sets can be told apart by inspecting a *single* 8-bit window of
917// the key bytes -- and that window need not be byte-aligned. Because every JSON
918// key is terminated by a '"', the bytes at and before a key's length are well
919// defined for any key at least that long: byte i is the key character when i is
920// inside the key and the closing quote when i == len. So we read two bytes at a
921// fixed offset, extract 8 consecutive bits at a fixed intra-byte shift, and if
922// that value is distinct for every key, a 256-entry table maps it straight to a
923// candidate key. The match then needs no hash and -- in the length-free overload
924// -- no SIMD length scan: load two bytes, shift, mask, index the table, and
925// confirm the candidate with one comparison.
926//
927// Allowing an *unaligned* window (a shift of 1..7) mixes bits from two adjacent
928// bytes and discriminates key sets that no single aligned byte can. For example,
929// the partial_tweets keys {created_at,id,text,in_reply_to_status_id,user,
930// retweet_count,favorite_count} share a colliding byte at every aligned position
931// 0,1,2, yet the 8 bits starting at bit offset 2 are unique across all seven.
932// Simpler cases fall out as the shift==0 special case: {"id","screen_name"}
933// splits on byte 0, {"jo","joe"} on the quote at byte 2.
934//
935// The window is confined to the first (shortest key length + 1) bytes so the
936// two-byte read never crosses a key's closing quote into uncontrolled value
937// bytes; that final byte is the shortest key's quote.
938template <std::size_t N, std::size_t MaxKeyLen>
939struct window_data {
940 static constexpr std::size_t KEY_STRIDE = ((MaxKeyLen + 15) / 16) * 16;
941 bool ok{false};
942 std::uint8_t byte_offset{0}; // first byte of the 2-byte read
943 std::uint8_t shift{0}; // intra-byte bit shift (0..7)
944 std::array<std::uint8_t, 256> window_to_key{}; // window byte -> key index, N if none
945 std::array<std::uint8_t, N> key_len{};
946 std::array<std::array<char, KEY_STRIDE>, N> key_bytes{}; // zero-padded
947};
948
949// Byte seen at `idx` for key `i`: a key character when idx is inside the key, or
950// the closing '"' when idx == len (callers keep idx <= every key's length).
951template <std::size_t N>
952constexpr unsigned window_byte_at(const std::array<std::string_view, N>& keys,
953 std::size_t i, std::size_t idx) noexcept {
954 if (idx < keys[i].size()) { return static_cast<unsigned char>(keys[i][idx]); }
955 return static_cast<unsigned>('"');
956}
957
958// The 8-bit window value for key `i` at (byte_offset, shift): the little-endian
959// pair (byte[off], byte[off+1]) shifted right by `shift` and truncated. Matches
960// the runtime read exactly.
961template <std::size_t N>
962constexpr unsigned window_value(const std::array<std::string_view, N>& keys,
963 std::size_t i, std::size_t byte_offset, std::size_t shift) noexcept {
964 unsigned lo = window_byte_at<N>(keys, i, byte_offset);
965 unsigned hi = window_byte_at<N>(keys, i, byte_offset + 1);
966 return ((lo | (hi << 8)) >> shift) & 0xFFu;
967}
968
969template <std::size_t N, std::size_t MaxKeyLen>
970consteval window_data<N, MaxKeyLen>
971compute_window(const std::array<std::string_view, N>& keys) {
972 window_data<N, MaxKeyLen> out{};
973
974 std::size_t min_len = keys[0].size();
975 for (std::size_t i = 1; i < N; ++i) {
976 if (keys[i].size() < min_len) { min_len = keys[i].size(); }
977 }
978
979 // Iterate windows nearest the front first (cheapest to read, smallest shift).
980 for (std::size_t off = 0; off <= min_len; ++off) {
981 for (std::size_t shift = 0; shift < 8; ++shift) {
982 // The read touches byte off, and byte off+1 when shift != 0. Both must
983 // stay within the safe region [0, min_len] (min_len is the shortest
984 // key's quote index). off <= min_len is guaranteed by the loop bound.
985 if (shift != 0 && off + 1 > min_len) { continue; }
986
987 bool distinct = true;
988 for (std::size_t i = 0; i < N && distinct; ++i) {
989 for (std::size_t j = i + 1; j < N; ++j) {
990 if (window_value<N>(keys, i, off, shift) == window_value<N>(keys, j, off, shift)) {
991 distinct = false;
992 break;
993 }
994 }
995 }
996 if (!distinct) { continue; }
997
998 out.ok = true;
999 out.byte_offset = static_cast<std::uint8_t>(off);
1000 out.shift = static_cast<std::uint8_t>(shift);
1001 for (std::size_t b = 0; b < 256; ++b) { out.window_to_key[b] = static_cast<std::uint8_t>(N); }
1002 for (std::size_t i = 0; i < N; ++i) {
1003 out.window_to_key[window_value<N>(keys, i, off, shift)] = static_cast<std::uint8_t>(i);
1004 out.key_len[i] = static_cast<std::uint8_t>(keys[i].size());
1005 for (std::size_t c = 0; c < keys[i].size(); ++c) { out.key_bytes[i][c] = keys[i][c]; }
1006 }
1007 return out;
1008 }
1009 }
1010 return out; // ok == false: no single 8-bit window distinguishes the keys
1011}
1012
1013// Read the 8-bit window at (byte_offset, shift) from a padded key pointer.
1014simdjson_really_inline std::uint8_t read_window(const char* p, std::size_t byte_offset,
1015 std::size_t shift) noexcept {
1016 std::uint16_t w;
1017 // Two controlled bytes (within the shortest key + its quote, hence within the
1018 // padded buffer). memcpy is the portable little-endian unaligned load.
1019 std::memcpy(&w, p + byte_offset, sizeof(w));
1020#if defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)
1021 w = static_cast<std::uint16_t>((w >> 8) | (w << 8));
1022#endif
1023 return static_cast<std::uint8_t>((w >> shift) & 0xFFu);
1024}
1025
1026// Verify a window candidate. The window table already mapped the key to the
1027// single possible index `ki` (< N); here we confirm it. Folding over 0..N-1
1028// turns the runtime `ki` into a compile-time index in the matching arm, so the
1029// candidate's length and bytes are constants for compare_key_bytes -- the same
1030// specialization the ordered find_field path gets from a CT-length
1031// unsafe_is_equal, and what keeps this path competitive. The closing-quote check
1032// (p[L] == '"') both confirms the key ends exactly at the candidate's length and
1033// rejects a wrong-length key, so this works whether or not the caller knew len.
1034template <std::size_t N, std::size_t MaxKeyLen, std::size_t... Is>
1035simdjson_really_inline std::size_t
1036match_window_candidate(const char* p, std::uint8_t ki,
1037 const window_data<N, MaxKeyLen>& w,
1038 std::index_sequence<Is...>) noexcept {
1039 std::size_t result = N;
1040 auto try_match = [&](auto Ic) {
1041 constexpr std::size_t i = decltype(Ic)::value;
1042 if (ki == i && p[w.key_len[i]] == '"' &&
1043 key_selector_detail::compare_key_bytes<MaxKeyLen>(
1044 p, w.key_bytes[i].data(), w.key_len[i])) {
1045 result = i;
1046 }
1047 };
1048 (try_match(std::integral_constant<std::size_t, Is>{}), ...);
1049 return result;
1050}
1051
1052// --- describe() string helpers (constexpr; no std::to_string, which is not) ---
1053
1054// Append the decimal form of v to s.
1055SIMDJSON_CONSTEXPR_STRING void append_uint(std::string& s, std::size_t v) {
1056 if (v == 0) { s.push_back('0'); return; }
1057 char buf[20];
1058 std::size_t n = 0;
1059 while (v > 0) { buf[n++] = static_cast<char>('0' + (v % 10)); v /= 10; }
1060 while (n > 0) { s.push_back(buf[--n]); }
1061}
1062
1063// Append a byte as its decimal value, plus the printable character in quotes
1064// when it is in the printable ASCII range (e.g. "110 ('n')").
1065SIMDJSON_CONSTEXPR_STRING void append_byte(std::string& s, unsigned b) {
1066 append_uint(s, b);
1067 if (b >= 0x20 && b < 0x7f) {
1068 s += " ('";
1069 s.push_back(static_cast<char>(b));
1070 s += "')";
1071 }
1072}
1073
1074} // namespace key_selector_detail
1075
1096template <constevalutil::fixed_string... Keys>
1097struct key_selector {
1098 static constexpr std::size_t N = sizeof...(Keys);
1099 static_assert(N > 0, "key_selector requires at least one key");
1100 static_assert(N <= 255,"key_selector supports at most 255 keys");
1101
1102 static constexpr std::array<std::string_view, N> keys{ Keys.view()... };
1103 static constexpr std::size_t max_key_len = key_selector_detail::compute_max_key_len<N>(keys);
1104 static_assert(max_key_len <= SIMDJSON_PADDING,
1105 "key longer than SIMDJSON_PADDING is not supported");
1106 // The SIMD key-length scan covers offsets 0..63 (four 16-byte blocks), which
1107 // stays within the 64-byte padding guarantee. A 64-character key's closing
1108 // quote would land at offset 64 and be missed on NEON/SSE2/LSX while still
1109 // matching in scalar builds, so cap at 63 to keep implementations in agreement.
1110 static_assert(max_key_len <= 63,
1111 "key_selector keys must be at most 63 characters long");
1112
1113 static constexpr auto result = key_selector_detail::compute_phf<N>(keys);
1114 static constexpr std::size_t table_size = result.table_size;
1115
1116 static constexpr auto phf =
1117 key_selector_detail::build_phf_data<N, table_size, max_key_len>(keys, result);
1118
1119 // Single 8-bit-window discriminator (when one exists). Detected at compile
1120 // time and selected with `if constexpr` below, so the hash path is compiled
1121 // out for key sets that qualify, and this is compiled out for those that do
1122 // not.
1123 static constexpr auto window =
1124 key_selector_detail::compute_window<N, max_key_len>(keys);
1125
1126 static constexpr std::size_t size() noexcept { return N; }
1127
1138 static simdjson_really_inline std::size_t match_raw(const char* p, std::size_t len) noexcept {
1139 if (len == 0 || len > max_key_len) { return N; }
1140
1141 if constexpr (window.ok) {
1142 // One 8-bit window selects the only possible candidate key;
1143 // match_window_candidate confirms it (bytes + closing quote). p sits
1144 // in a padded buffer and the window stays within the shortest key +
1145 // quote, so the two-byte read is always in bounds. len is unused here
1146 // because the quote check already pins the key's end.
1147 std::uint8_t ki = window.window_to_key[
1148 key_selector_detail::read_window(p, window.byte_offset, window.shift)];
1149 if (ki >= N) { return N; }
1150 return key_selector_detail::match_window_candidate(
1151 p, ki, window, std::make_index_sequence<N>{});
1152 }
1153
1154 std::size_t slot;
1155 if (phf.num_positions == key_selector_detail::HD_MODE) {
1156 // Hash-and-Displace: bucket displacement + per-key hash.
1157 std::string_view key(p, len);
1158 std::size_t bucket = key_selector_detail::hd_bucket_hash(key);
1159 std::size_t kh = (phf.hd_hash_variant == key_selector_detail::HD_HASH_2BYTE_FLAG)
1160 ? key_selector_detail::hd_key_hash_2(key)
1161 : key_selector_detail::hd_key_hash_4(key);
1162 slot = (phf.asso_values[0][bucket] + kh) & (table_size - 1);
1163 } else {
1164 // gperf: h = len + sum of asso_values over the selected positions.
1165 // positions / num_positions / asso_values are compile-time constants,
1166 // so this loop fully unrolls. The idx < len guard mirrors the
1167 // generator's char_at()-> 256 -> skip behavior for out-of-range
1168 // positions (required: arbitrary positions may exceed a key's length).
1169 std::size_t h = len;
1170 for (std::uint8_t i = 0; i < phf.num_positions; ++i) {
1171 std::uint8_t pos = phf.positions[i];
1172 std::size_t idx = (pos == key_selector_detail::POS_LAST_CHAR)
1173 ? (len - std::size_t{1})
1174 : static_cast<std::size_t>(pos);
1175 if (idx < len) {
1176 h += phf.asso_values[i][static_cast<unsigned char>(p[idx])];
1177 }
1178 }
1179 slot = h & (table_size - 1);
1180 }
1181
1182 std::uint8_t ki = phf.slot_to_key[slot];
1183 if (ki >= N) { return N; }
1184 if (phf.slot_key_len[slot] != len) { return N; }
1185 if (!key_selector_detail::compare_key_bytes<max_key_len>(
1186 p, phf.slot_key_bytes[slot].data(), len)) { return N; }
1187 return ki;
1188 }
1189
1196 static simdjson_really_inline std::size_t match_raw(raw_json_string rjs) noexcept {
1197 const char* p = rjs.raw();
1198 if constexpr (window.ok) {
1199 // One 8-bit window picks the candidate; verifying the candidate's
1200 // bytes and its closing '"' confirms the full key, so the length scan
1201 // is unnecessary. The window read is in bounds (padding), and the
1202 // candidate length is at most max_key_len.
1203 std::uint8_t ki = window.window_to_key[
1204 key_selector_detail::read_window(p, window.byte_offset, window.shift)];
1205 if (ki >= N) { return N; }
1206 return key_selector_detail::match_window_candidate(
1207 p, ki, window, std::make_index_sequence<N>{});
1208 }
1209 return match_raw(p, key_selector_detail::scan_key_length<max_key_len>(p));
1210 }
1211
1213 static constexpr std::string_view key_at(std::size_t i) noexcept {
1214 return keys[i];
1215 }
1216
1234 static SIMDJSON_CONSTEXPR_STRING std::string describe() {
1235 std::string s;
1236 s += "key_selector: ";
1237 key_selector_detail::append_uint(s, N);
1238 s += " keys, max key length ";
1239 key_selector_detail::append_uint(s, max_key_len);
1240 s += "\nkeys:\n";
1241 for (std::size_t i = 0; i < N; ++i) {
1242 s += " [";
1243 key_selector_detail::append_uint(s, i);
1244 s += "] \"";
1245 s += keys[i];
1246 s += "\" (length ";
1247 key_selector_detail::append_uint(s, keys[i].size());
1248 s += ")\n";
1249 }
1250 if constexpr (window.ok) {
1251 // Mirrors the window fast path of match_raw().
1252 s += "algorithm: single 8-bit window\n";
1253 s += " step 1: read 2 bytes at offset ";
1254 key_selector_detail::append_uint(s, window.byte_offset);
1255 s += ", interpret them as a little-endian 16-bit value, shift right by ";
1256 key_selector_detail::append_uint(s, window.shift);
1257 s += " bits, and keep the low 8 bits\n";
1258 s += " step 2: map that byte through a 256-entry table to a key index (";
1259 key_selector_detail::append_uint(s, N);
1260 s += " means no match):\n";
1261 for (std::size_t b = 0; b < 256; ++b) {
1262 if (window.window_to_key[b] < N) {
1263 s += " byte ";
1264 key_selector_detail::append_byte(s, static_cast<unsigned>(b));
1265 s += " -> key ";
1266 key_selector_detail::append_uint(s, window.window_to_key[b]);
1267 s += "\n";
1268 }
1269 }
1270 s += " step 3: confirm the candidate by checking the closing quote sits at the key's length and comparing the key bytes\n";
1271 } else {
1272 // Mirrors the perfect-hash path of match_raw().
1273 if constexpr (phf.num_positions == key_selector_detail::HD_MODE) {
1274 s += "algorithm: hash-and-displace perfect hash\n";
1275 s += " step 1: bucket = (first_byte + last_byte*3 + length*17) mod 256\n";
1276 s += " step 2: keyhash = base-31 rolling hash of the length and the first ";
1277 key_selector_detail::append_uint(s, phf.hd_hash_variant);
1278 s += " bytes\n";
1279 s += " step 3: slot = (displacement[bucket] + keyhash) mod ";
1280 key_selector_detail::append_uint(s, table_size);
1281 s += "\n step 4: slot_to_key[slot] gives the candidate key index\n";
1282 s += " per-key derivation:\n";
1283 for (std::size_t i = 0; i < N; ++i) {
1284 std::string_view k = keys[i];
1285 std::size_t bucket = key_selector_detail::hd_bucket_hash(k);
1286 std::size_t kh = (phf.hd_hash_variant == key_selector_detail::HD_HASH_2BYTE_FLAG)
1287 ? key_selector_detail::hd_key_hash_2(k)
1288 : key_selector_detail::hd_key_hash_4(k);
1289 std::size_t slot = (phf.asso_values[0][bucket] + kh) & (table_size - 1);
1290 s += " \"";
1291 s += k;
1292 s += "\": bucket=";
1293 key_selector_detail::append_uint(s, bucket);
1294 s += " displacement=";
1295 key_selector_detail::append_uint(s, phf.asso_values[0][bucket]);
1296 s += " keyhash=";
1297 key_selector_detail::append_uint(s, kh);
1298 s += " slot=";
1299 key_selector_detail::append_uint(s, slot);
1300 s += "\n";
1301 }
1302 } else {
1303 s += "algorithm: gperf-style perfect hash over ";
1304 key_selector_detail::append_uint(s, phf.num_positions);
1305 s += " character position(s)\n";
1306 s += " step 1: h = key length\n";
1307 s += " step 2: for each position below, add its association value for the key byte there (a position past the key's length contributes 0):\n";
1308 for (std::size_t i = 0; i < phf.num_positions; ++i) {
1309 s += " position ";
1310 if (phf.positions[i] == key_selector_detail::POS_LAST_CHAR) {
1311 s += "last character";
1312 } else {
1313 s += "byte index ";
1314 key_selector_detail::append_uint(s, phf.positions[i]);
1315 }
1316 s += "\n";
1317 }
1318 s += " step 3: slot = h mod ";
1319 key_selector_detail::append_uint(s, table_size);
1320 s += " (a power of two, applied as a bitmask)\n";
1321 s += " step 4: slot_to_key[slot] gives the candidate key index\n";
1322 s += " per-key derivation:\n";
1323 for (std::size_t i = 0; i < N; ++i) {
1324 std::string_view k = keys[i];
1325 std::size_t h = k.size();
1326 for (std::size_t pi = 0; pi < phf.num_positions; ++pi) {
1327 std::size_t pos = phf.positions[pi];
1328 std::size_t idx = (pos == key_selector_detail::POS_LAST_CHAR)
1329 ? (k.size() - 1) : pos;
1330 if (idx < k.size()) {
1331 h += phf.asso_values[pi][static_cast<unsigned char>(k[idx])];
1332 }
1333 }
1334 std::size_t slot = h & (table_size - 1);
1335 s += " \"";
1336 s += k;
1337 s += "\": h=";
1338 key_selector_detail::append_uint(s, h);
1339 s += " slot=";
1340 key_selector_detail::append_uint(s, slot);
1341 s += "\n";
1342 }
1343 }
1344 s += " occupied slots (slot -> key):\n";
1345 for (std::size_t slot = 0; slot < table_size; ++slot) {
1346 if (phf.slot_to_key[slot] < N) {
1347 s += " slot ";
1348 key_selector_detail::append_uint(s, slot);
1349 s += " -> key ";
1350 key_selector_detail::append_uint(s, phf.slot_to_key[slot]);
1351 s += " (\"";
1352 s += keys[phf.slot_to_key[slot]];
1353 s += "\", length ";
1354 key_selector_detail::append_uint(s, phf.slot_key_len[slot]);
1355 s += ")\n";
1356 }
1357 }
1358 s += " confirm the candidate by checking the key length matches and comparing the key bytes\n";
1359 }
1360 return s;
1361 }
1362};
1363
1364namespace key_selector_detail {
1365template <typename> struct is_key_selector : std::false_type {};
1366template <constevalutil::fixed_string... Keys>
1367struct is_key_selector<key_selector<Keys...>> : std::true_type {};
1368} // namespace key_selector_detail
1369
1375template <typename T>
1376concept key_selector_type = key_selector_detail::is_key_selector<T>::value;
1377
1378} // namespace ondemand
1379} // namespace SIMDJSON_IMPLEMENTATION
1380} // namespace simdjson
1381
1382#endif // SIMDJSON_SUPPORTS_CONCEPTS
1383
1384#endif // SIMDJSON_GENERIC_ONDEMAND_KEY_SELECTOR_H
The top level simdjson namespace, containing everything the library provides.
Definition base.h:8
constexpr size_t SIMDJSON_PADDING
The amount of padding needed in a buffer to parse JSON.
Definition base.h:42