mirror of
https://github.com/simdjson/simdjson
synced 2026-06-08 17:27:07 +00:00
931 lines
38 KiB
C++
931 lines
38 KiB
C++
#ifndef SIMDJSON_GENERIC_ONDEMAND_KEY_SELECTOR_H
|
|
#define SIMDJSON_GENERIC_ONDEMAND_KEY_SELECTOR_H
|
|
|
|
#ifndef SIMDJSON_CONDITIONAL_INCLUDE
|
|
#include "simdjson/base.h"
|
|
#include "simdjson/common_defs.h"
|
|
#include "simdjson/constevalutil.h"
|
|
#include "simdjson/generic/ondemand/raw_json_string.h"
|
|
#endif // SIMDJSON_CONDITIONAL_INCLUDE
|
|
|
|
#include <array>
|
|
#include <string_view>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
|
|
#if defined(__aarch64__) || defined(__ARM_NEON)
|
|
#include <arm_neon.h>
|
|
#define SIMDJSON_KEY_SELECTOR_HAS_NEON 1
|
|
#else
|
|
#define SIMDJSON_KEY_SELECTOR_HAS_NEON 0
|
|
#endif
|
|
#if defined(__SSE2__)
|
|
#include <emmintrin.h>
|
|
#define SIMDJSON_KEY_SELECTOR_HAS_SSE2 1
|
|
#else
|
|
#define SIMDJSON_KEY_SELECTOR_HAS_SSE2 0
|
|
#endif
|
|
|
|
#if SIMDJSON_SUPPORTS_CONCEPTS
|
|
|
|
namespace simdjson {
|
|
namespace SIMDJSON_IMPLEMENTATION {
|
|
namespace ondemand {
|
|
|
|
namespace key_selector_detail {
|
|
|
|
// ============================================================================
|
|
// Compile-time perfect-hash generator.
|
|
//
|
|
// This is a port of the ConstexprCore perfect-hash generator
|
|
// (https://github.com/ConstexprCore/perfect_hash). It scales to ~100 keys at
|
|
// compile time by determining association values one (position, character)
|
|
// symbol at a time (gperf-style) instead of an exhaustive offset search, and
|
|
// falls back to a Hash-and-Displace construction for large/awkward key sets.
|
|
//
|
|
// Only flat tables survive to runtime; the lookup is a few additions plus a
|
|
// single SIMD key comparison (see match_raw below).
|
|
// ============================================================================
|
|
|
|
// Maximum number of character positions the gperf hash may combine.
|
|
static constexpr std::size_t MAX_POSITIONS = 16;
|
|
// Sentinel "position" meaning "the last character of the key".
|
|
static constexpr std::size_t LAST_CHAR = std::size_t(-1);
|
|
// Runtime-encoded sentinels (stored in uint8 tables).
|
|
static constexpr std::uint8_t POS_LAST_CHAR = 0xFF; // positions_[i] == last char
|
|
static constexpr std::uint8_t HD_MODE = 0xFF; // num_positions == H&D mode
|
|
// Flags stored in positions[2] in H&D mode to select the key-hash variant.
|
|
static constexpr std::size_t HD_HASH_2BYTE_FLAG = 2;
|
|
static constexpr std::size_t HD_HASH_4BYTE_FLAG = 4;
|
|
|
|
constexpr std::size_t next_power_of_2(std::size_t n) noexcept {
|
|
if (n == 0) { return 1; }
|
|
std::size_t p = 1;
|
|
while (p < n) { p <<= 1; }
|
|
return p;
|
|
}
|
|
|
|
// Character at a given position (LAST_CHAR means last character), or 256 if out
|
|
// of bounds.
|
|
constexpr std::size_t char_at(std::string_view key, std::size_t pos) noexcept {
|
|
if (pos == LAST_CHAR) {
|
|
if (key.empty()) { return 256; }
|
|
return static_cast<unsigned char>(key[key.size() - 1]);
|
|
}
|
|
if (pos >= key.size()) { return 256; }
|
|
return static_cast<unsigned char>(key[pos]);
|
|
}
|
|
|
|
// Count key pairs that a set of positions fails to distinguish. Keys whose
|
|
// lengths differ modulo the table size are separated by the length term in the
|
|
// hash, so they need no position coverage.
|
|
template <std::size_t N>
|
|
consteval std::size_t count_undistinguished_pairs(
|
|
const std::array<std::string_view, N>& keys,
|
|
const std::size_t* positions,
|
|
std::size_t num_positions,
|
|
std::size_t modulus) {
|
|
std::size_t count = 0;
|
|
for (std::size_t i = 0; i < N; ++i) {
|
|
for (std::size_t j = i + 1; j < N; ++j) {
|
|
if (keys[i].size() % modulus != keys[j].size() % modulus) { continue; }
|
|
bool distinguished = false;
|
|
for (std::size_t p = 0; p < num_positions; ++p) {
|
|
if (char_at(keys[i], positions[p]) != char_at(keys[j], positions[p])) {
|
|
distinguished = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!distinguished) { ++count; }
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
|
|
template <std::size_t N>
|
|
consteval bool positions_distinguish(
|
|
const std::array<std::string_view, N>& keys,
|
|
const std::size_t* positions,
|
|
std::size_t num_positions,
|
|
std::size_t modulus) {
|
|
return count_undistinguished_pairs<N>(keys, positions, num_positions, modulus) == 0;
|
|
}
|
|
|
|
// Number of distinct (length % modulus, char_at(key, pos)) pairs at a position.
|
|
template <std::size_t N>
|
|
consteval std::size_t discriminating_power(
|
|
const std::array<std::string_view, N>& keys,
|
|
std::size_t pos,
|
|
std::size_t modulus) {
|
|
struct pair { std::size_t len_mod; std::size_t ch; };
|
|
std::array<pair, N> pairs{};
|
|
for (std::size_t i = 0; i < N; ++i) {
|
|
pairs[i] = {keys[i].size() % modulus, char_at(keys[i], pos)};
|
|
}
|
|
std::size_t count = 0;
|
|
for (std::size_t i = 0; i < N; ++i) {
|
|
bool dup = false;
|
|
for (std::size_t j = 0; j < i; ++j) {
|
|
if (pairs[i].len_mod == pairs[j].len_mod && pairs[i].ch == pairs[j].ch) {
|
|
dup = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!dup) { ++count; }
|
|
}
|
|
return count;
|
|
}
|
|
|
|
template <std::size_t N>
|
|
consteval std::size_t max_key_length(const std::array<std::string_view, N>& keys) {
|
|
std::size_t m = 0;
|
|
for (std::size_t i = 0; i < N; ++i) {
|
|
if (keys[i].size() > m) { m = keys[i].size(); }
|
|
}
|
|
return m;
|
|
}
|
|
|
|
// Bounded backtracking DFS for a minimal set of distinguishing positions.
|
|
template <std::size_t N>
|
|
consteval bool backtracking_search(
|
|
const std::array<std::string_view, N>& keys,
|
|
const std::size_t* candidates,
|
|
std::size_t num_candidates,
|
|
std::size_t* positions,
|
|
std::size_t& num_positions_out,
|
|
std::size_t& budget,
|
|
std::size_t modulus) {
|
|
constexpr std::size_t MAX_DEPTH = 8;
|
|
std::size_t breadth = num_candidates < 20 ? num_candidates : 20;
|
|
|
|
struct frame { std::size_t depth; std::size_t next_ci; std::size_t parent_count; };
|
|
std::array<frame, MAX_DEPTH + 1> stack{};
|
|
std::size_t sp = 0;
|
|
|
|
std::size_t initial_count = count_undistinguished_pairs<N>(keys, positions, 0, modulus);
|
|
if (budget > 0) { --budget; }
|
|
if (initial_count == 0) { num_positions_out = 0; return true; }
|
|
|
|
stack[0] = {0, 0, initial_count};
|
|
|
|
while (budget > 0) {
|
|
if (sp > MAX_DEPTH) {
|
|
if (sp == 0) { break; }
|
|
--sp;
|
|
++stack[sp].next_ci;
|
|
continue;
|
|
}
|
|
auto& f = stack[sp];
|
|
if (f.next_ci >= breadth) {
|
|
if (sp == 0) { break; }
|
|
--sp;
|
|
++stack[sp].next_ci;
|
|
continue;
|
|
}
|
|
positions[sp] = candidates[f.next_ci];
|
|
--budget;
|
|
std::size_t new_count = count_undistinguished_pairs<N>(keys, positions, sp + 1, modulus);
|
|
if (new_count == 0) { num_positions_out = sp + 1; return true; }
|
|
if (new_count < f.parent_count && sp + 1 < MAX_DEPTH) {
|
|
stack[sp + 1] = {sp + 1, f.next_ci + 1, new_count};
|
|
++sp;
|
|
} else {
|
|
++f.next_ci;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Phase 1: select character positions that distinguish all colliding pairs.
|
|
template <std::size_t N>
|
|
consteval std::size_t select_positions(
|
|
const std::array<std::string_view, N>& keys,
|
|
std::array<std::size_t, MAX_POSITIONS>& positions,
|
|
std::size_t modulus) {
|
|
if (positions_distinguish<N>(keys, positions.data(), 0, modulus)) { return 0; }
|
|
|
|
std::size_t max_len = max_key_length(keys);
|
|
constexpr std::size_t MAX_CANDIDATES = 256;
|
|
std::array<std::size_t, MAX_CANDIDATES> candidates{};
|
|
std::array<std::size_t, MAX_CANDIDATES> powers{};
|
|
std::size_t num_candidates = 0;
|
|
for (std::size_t p = 0; p < max_len && num_candidates < MAX_CANDIDATES - 1; ++p) {
|
|
candidates[num_candidates] = p;
|
|
powers[num_candidates] = discriminating_power(keys, p, modulus);
|
|
++num_candidates;
|
|
}
|
|
if (num_candidates < MAX_CANDIDATES) {
|
|
candidates[num_candidates] = LAST_CHAR;
|
|
powers[num_candidates] = discriminating_power(keys, LAST_CHAR, modulus);
|
|
++num_candidates;
|
|
}
|
|
for (std::size_t i = 0; i < num_candidates; ++i) {
|
|
for (std::size_t j = i + 1; j < num_candidates; ++j) {
|
|
if (powers[j] > powers[i]) {
|
|
auto tc = candidates[i]; candidates[i] = candidates[j]; candidates[j] = tc;
|
|
auto tp = powers[i]; powers[i] = powers[j]; powers[j] = tp;
|
|
}
|
|
}
|
|
}
|
|
|
|
positions[0] = candidates[0];
|
|
if (positions_distinguish<N>(keys, positions.data(), 1, modulus)) { return 1; }
|
|
|
|
positions[0] = 0;
|
|
positions[1] = LAST_CHAR;
|
|
if (positions_distinguish<N>(keys, positions.data(), 2, modulus)) { return 2; }
|
|
|
|
{
|
|
std::size_t budget = 5000;
|
|
std::size_t num_found = 0;
|
|
if (backtracking_search<N>(keys, candidates.data(), num_candidates,
|
|
positions.data(), num_found, budget, modulus)) {
|
|
return num_found;
|
|
}
|
|
}
|
|
|
|
std::size_t num_pos = 0;
|
|
for (std::size_t ci = 0; ci < num_candidates && num_pos < MAX_POSITIONS; ++ci) {
|
|
bool already = false;
|
|
for (std::size_t p = 0; p < num_pos; ++p) {
|
|
if (positions[p] == candidates[ci]) { already = true; break; }
|
|
}
|
|
if (already) { continue; }
|
|
positions[num_pos] = candidates[ci];
|
|
++num_pos;
|
|
if (positions_distinguish<N>(keys, positions.data(), num_pos, modulus)) { return num_pos; }
|
|
}
|
|
|
|
throw "Failed to find distinguishing positions for perfect hash";
|
|
}
|
|
|
|
// Result of PHF computation. A max-sized slot_to_key array lets the same struct
|
|
// type carry any chosen table size.
|
|
template <std::size_t N>
|
|
struct phf_result {
|
|
// Allow up to 8x the minimum table size. Sparser tables solve faster.
|
|
static constexpr std::size_t MAX_TABLE_SIZE = next_power_of_2(N) * 8;
|
|
std::size_t table_size{};
|
|
std::array<std::array<std::size_t, 256>, MAX_POSITIONS> asso_values{};
|
|
std::size_t num_positions{};
|
|
std::array<std::size_t, MAX_POSITIONS> positions{};
|
|
std::array<std::size_t, MAX_TABLE_SIZE> slot_to_key{};
|
|
};
|
|
|
|
// Partition-based asso_values search (gperf-style). Determines asso_values one
|
|
// (position, character) symbol at a time; never revisits a value. Equivalence
|
|
// classes (keys sharing the same undetermined symbols) keep the search cheap.
|
|
template <std::size_t N, std::size_t M>
|
|
consteval bool try_generate_gperf(
|
|
const std::array<std::string_view, N>& keys,
|
|
std::array<std::array<std::size_t, 256>, MAX_POSITIONS>& asso_values,
|
|
std::size_t& num_positions,
|
|
std::array<std::size_t, MAX_POSITIONS>& positions,
|
|
std::array<std::size_t, M>& slot_to_key) {
|
|
num_positions = select_positions<N>(keys, positions, M);
|
|
|
|
for (std::size_t p = 0; p < MAX_POSITIONS; ++p) {
|
|
for (std::size_t c = 0; c < 256; ++c) { asso_values[p][c] = 0; }
|
|
}
|
|
|
|
if (num_positions == 0) {
|
|
for (std::size_t i = 0; i < M; ++i) { slot_to_key[i] = N; }
|
|
for (std::size_t i = 0; i < N; ++i) {
|
|
std::size_t slot = keys[i].size() % M;
|
|
if (slot_to_key[slot] != N) { return false; }
|
|
slot_to_key[slot] = i;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
std::array<std::array<std::size_t, MAX_POSITIONS>, N> kchars{};
|
|
for (std::size_t k = 0; k < N; ++k) {
|
|
for (std::size_t p = 0; p < num_positions; ++p) {
|
|
kchars[k][p] = char_at(keys[k], positions[p]);
|
|
}
|
|
}
|
|
|
|
struct sym_t { std::size_t pos; std::size_t ch; std::size_t freq; };
|
|
constexpr std::size_t MAX_SYMS = MAX_POSITIONS * 256;
|
|
std::array<sym_t, MAX_SYMS> syms{};
|
|
std::size_t nsyms = 0;
|
|
for (std::size_t p = 0; p < num_positions; ++p) {
|
|
std::array<std::size_t, 256> freq{};
|
|
for (std::size_t k = 0; k < N; ++k) {
|
|
std::size_t c = kchars[k][p];
|
|
if (c < 256) { freq[c]++; }
|
|
}
|
|
for (std::size_t c = 0; c < 256; ++c) {
|
|
if (freq[c] > 0) { syms[nsyms++] = {p, c, freq[c]}; }
|
|
}
|
|
}
|
|
for (std::size_t i = 0; i < nsyms; ++i) {
|
|
for (std::size_t j = i + 1; j < nsyms; ++j) {
|
|
if (syms[j].freq > syms[i].freq) {
|
|
auto tmp = syms[i]; syms[i] = syms[j]; syms[j] = tmp;
|
|
}
|
|
}
|
|
}
|
|
|
|
std::array<std::size_t, N> phash{};
|
|
for (std::size_t k = 0; k < N; ++k) { phash[k] = keys[k].size(); }
|
|
|
|
// Equivalence-class signatures: sig[k] = XOR of a per-symbol salt over the
|
|
// key's undetermined symbols. Updated incrementally as symbols are fixed.
|
|
std::array<std::array<std::size_t, 256>, MAX_POSITIONS> salt{};
|
|
{
|
|
std::size_t s = 0x9e3779b97f4a7c15ULL;
|
|
for (std::size_t p = 0; p < num_positions; ++p) {
|
|
for (std::size_t c = 0; c < 256; ++c) {
|
|
s = s * 6364136223846793005ULL + 1442695040888963407ULL;
|
|
salt[p][c] = s;
|
|
}
|
|
}
|
|
}
|
|
std::array<std::size_t, N> sig{};
|
|
for (std::size_t k = 0; k < N; ++k) {
|
|
std::size_t s = 0;
|
|
for (std::size_t p = 0; p < num_positions; ++p) {
|
|
std::size_t c = kchars[k][p];
|
|
if (c < 256) { s ^= salt[p][c]; }
|
|
}
|
|
sig[k] = s;
|
|
}
|
|
std::array<std::size_t, N> order{};
|
|
for (std::size_t k = 0; k < N; ++k) { order[k] = k; }
|
|
|
|
std::array<std::size_t, M> slot_gen{};
|
|
std::size_t gen = 0;
|
|
|
|
std::size_t search_limit = next_power_of_2(M);
|
|
if (search_limit < 32) { search_limit = 32; }
|
|
|
|
for (std::size_t si = 0; si < nsyms; ++si) {
|
|
std::size_t sp = syms[si].pos;
|
|
std::size_t sc = syms[si].ch;
|
|
|
|
std::size_t sp_salt = salt[sp][sc];
|
|
for (std::size_t k = 0; k < N; ++k) {
|
|
if (kchars[k][sp] == sc) { sig[k] ^= sp_salt; }
|
|
}
|
|
|
|
for (std::size_t i = 1; i < N; ++i) {
|
|
std::size_t x = order[i];
|
|
std::size_t xs = sig[x];
|
|
std::size_t j = i;
|
|
while (j > 0 && sig[order[j - 1]] > xs) {
|
|
order[j] = order[j - 1];
|
|
--j;
|
|
}
|
|
order[j] = x;
|
|
}
|
|
|
|
bool found = false;
|
|
for (std::size_t v = 0; v < search_limit && !found; ++v) {
|
|
bool collision = false;
|
|
std::size_t ci = 0;
|
|
while (ci < N && !collision) {
|
|
std::size_t class_sig = sig[order[ci]];
|
|
std::size_t cj = ci;
|
|
while (cj < N && sig[order[cj]] == class_sig) { ++cj; }
|
|
if (cj - ci > 1) {
|
|
++gen;
|
|
for (std::size_t x = ci; x < cj; ++x) {
|
|
std::size_t k = order[x];
|
|
std::size_t h = phash[k];
|
|
if (kchars[k][sp] == sc) { h += v; }
|
|
h %= M;
|
|
if (slot_gen[h] == gen) { collision = true; break; }
|
|
slot_gen[h] = gen;
|
|
}
|
|
}
|
|
ci = cj;
|
|
}
|
|
if (!collision) {
|
|
asso_values[sp][sc] = v;
|
|
for (std::size_t k = 0; k < N; ++k) {
|
|
if (kchars[k][sp] == sc) { phash[k] += v; }
|
|
}
|
|
found = true;
|
|
}
|
|
}
|
|
if (!found) { return false; }
|
|
}
|
|
|
|
for (std::size_t i = 0; i < M; ++i) { slot_to_key[i] = N; }
|
|
for (std::size_t i = 0; i < N; ++i) {
|
|
std::size_t slot = phash[i] % M;
|
|
if (slot_to_key[slot] != N) { return false; }
|
|
slot_to_key[slot] = i;
|
|
}
|
|
std::size_t filled = 0;
|
|
for (std::size_t i = 0; i < M; ++i) {
|
|
if (slot_to_key[i] != N) { ++filled; }
|
|
}
|
|
return filled == N;
|
|
}
|
|
|
|
template <std::size_t N, std::size_t M>
|
|
consteval bool try_compute_phf(const std::array<std::string_view, N>& keys, phf_result<N>& result) {
|
|
static_assert(M <= phf_result<N>::MAX_TABLE_SIZE, "Table size M exceeds maximum");
|
|
std::array<std::array<std::size_t, 256>, MAX_POSITIONS> asso{};
|
|
std::size_t npos{};
|
|
std::array<std::size_t, MAX_POSITIONS> pos{};
|
|
std::array<std::size_t, M> s2k{};
|
|
if (try_generate_gperf<N, M>(keys, asso, npos, pos, s2k)) {
|
|
result.table_size = M;
|
|
result.asso_values = asso;
|
|
result.num_positions = npos;
|
|
result.positions = pos;
|
|
for (std::size_t i = 0; i < M; ++i) { result.slot_to_key[i] = s2k[i]; }
|
|
for (std::size_t i = M; i < phf_result<N>::MAX_TABLE_SIZE; ++i) { result.slot_to_key[i] = N; }
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
template <std::size_t N, std::size_t M, std::size_t MaxM>
|
|
consteval bool try_gperf_po2(const std::array<std::string_view, N>& keys, phf_result<N>& result) {
|
|
if (try_compute_phf<N, M>(keys, result)) { return true; }
|
|
constexpr std::size_t NextM = M * 2;
|
|
if constexpr (NextM <= MaxM) { return try_gperf_po2<N, NextM, MaxM>(keys, result); }
|
|
return false;
|
|
}
|
|
|
|
// --- Hash-and-Displace fallback --------------------------------------------
|
|
|
|
constexpr std::size_t hd_bucket_hash(std::string_view key) noexcept {
|
|
std::size_t c0 = key.empty() ? 0 : static_cast<unsigned char>(key[0]);
|
|
std::size_t c1 = key.empty() ? 0 : static_cast<unsigned char>(key[key.size() - 1]);
|
|
return (c0 + c1 * 3 + key.size() * 17) & 0xFF;
|
|
}
|
|
constexpr std::size_t hd_safe_char(const char* p, std::size_t len, std::size_t idx) noexcept {
|
|
std::size_t has = static_cast<std::size_t>(idx < len);
|
|
std::size_t si = idx & (std::size_t{0} - has);
|
|
return static_cast<unsigned char>(p[si]) & (std::size_t{0} - has);
|
|
}
|
|
constexpr std::size_t hd_key_hash_2(std::string_view key) noexcept {
|
|
std::size_t kc = key.size();
|
|
kc = kc * 31 + static_cast<unsigned char>(key[0]);
|
|
kc = kc * 31 + hd_safe_char(key.data(), key.size(), 1);
|
|
return kc;
|
|
}
|
|
constexpr std::size_t hd_key_hash_4(std::string_view key) noexcept {
|
|
std::size_t kc = key.size();
|
|
kc = kc * 31 + static_cast<unsigned char>(key[0]);
|
|
kc = kc * 31 + hd_safe_char(key.data(), key.size(), 1);
|
|
kc = kc * 31 + hd_safe_char(key.data(), key.size(), 2);
|
|
kc = kc * 31 + hd_safe_char(key.data(), key.size(), 3);
|
|
return kc;
|
|
}
|
|
|
|
template <std::size_t N, std::size_t M>
|
|
consteval bool try_hash_and_displace(
|
|
const std::array<std::string_view, N>& keys,
|
|
std::array<std::array<std::size_t, 256>, MAX_POSITIONS>& asso_values,
|
|
std::size_t& num_positions,
|
|
std::array<std::size_t, MAX_POSITIONS>& positions,
|
|
std::array<std::size_t, M>& slot_to_key) {
|
|
num_positions = select_positions<N>(keys, positions, M);
|
|
|
|
if (num_positions == 0) {
|
|
for (std::size_t i = 0; i < 256; ++i) { asso_values[0][i] = 0; }
|
|
for (std::size_t i = 0; i < M; ++i) { slot_to_key[i] = N; }
|
|
for (std::size_t i = 0; i < N; ++i) {
|
|
std::size_t slot = keys[i].size() % M;
|
|
if (slot_to_key[slot] != N) { return false; }
|
|
slot_to_key[slot] = i;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
for (std::size_t i = 0; i < 256; ++i) { asso_values[0][i] = 0; }
|
|
num_positions = HD_MODE; // sentinel for H&D mode
|
|
positions[0] = 0;
|
|
positions[1] = LAST_CHAR;
|
|
|
|
std::array<std::size_t, N> key_bucket{};
|
|
for (std::size_t i = 0; i < N; ++i) { key_bucket[i] = hd_bucket_hash(keys[i]); }
|
|
|
|
struct bucket_info { std::size_t ch; std::size_t count; };
|
|
std::array<bucket_info, N> buckets{};
|
|
std::size_t num_buckets = 0;
|
|
for (std::size_t i = 0; i < N; ++i) {
|
|
std::size_t bk = key_bucket[i];
|
|
bool found = false;
|
|
for (std::size_t b = 0; b < num_buckets; ++b) {
|
|
if (buckets[b].ch == bk) { ++buckets[b].count; found = true; break; }
|
|
}
|
|
if (!found) { buckets[num_buckets++] = {bk, 1}; }
|
|
}
|
|
for (std::size_t i = 0; i < num_buckets; ++i) {
|
|
for (std::size_t j = i + 1; j < num_buckets; ++j) {
|
|
if (buckets[j].count > buckets[i].count) {
|
|
auto tmp = buckets[i]; buckets[i] = buckets[j]; buckets[j] = tmp;
|
|
}
|
|
}
|
|
}
|
|
|
|
auto try_placement = [&](auto key_hash_fn) -> bool {
|
|
for (std::size_t i = 0; i < M; ++i) { slot_to_key[i] = N; }
|
|
for (std::size_t i = 0; i < 256; ++i) { asso_values[0][i] = 0; }
|
|
for (std::size_t b = 0; b < num_buckets; ++b) {
|
|
std::size_t ch = buckets[b].ch;
|
|
std::array<std::size_t, N> bucket_keys{};
|
|
std::size_t bk_count = 0;
|
|
for (std::size_t i = 0; i < N; ++i) {
|
|
if (key_bucket[i] == ch) { bucket_keys[bk_count++] = i; }
|
|
}
|
|
bool placed = false;
|
|
std::size_t max_d = M < 255 ? M : 255;
|
|
for (std::size_t d = 0; d < max_d; ++d) {
|
|
bool ok = true;
|
|
std::array<std::size_t, N> bucket_slots{};
|
|
for (std::size_t k = 0; k < bk_count; ++k) {
|
|
std::size_t slot = (d + key_hash_fn(keys[bucket_keys[k]])) % M;
|
|
if (slot_to_key[slot] != N) { ok = false; break; }
|
|
for (std::size_t k2 = 0; k2 < k; ++k2) {
|
|
if (bucket_slots[k2] == slot) { ok = false; break; }
|
|
}
|
|
if (!ok) { break; }
|
|
bucket_slots[k] = slot;
|
|
}
|
|
if (ok) {
|
|
asso_values[0][ch] = d;
|
|
for (std::size_t k = 0; k < bk_count; ++k) {
|
|
slot_to_key[bucket_slots[k]] = bucket_keys[k];
|
|
}
|
|
placed = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!placed) { return false; }
|
|
}
|
|
std::size_t filled = 0;
|
|
for (std::size_t i = 0; i < M; ++i) {
|
|
if (slot_to_key[i] != N) { ++filled; }
|
|
}
|
|
return filled == N;
|
|
};
|
|
|
|
if (try_placement([](std::string_view k) { return hd_key_hash_2(k); })) {
|
|
positions[2] = HD_HASH_2BYTE_FLAG;
|
|
return true;
|
|
}
|
|
if (try_placement([](std::string_view k) { return hd_key_hash_4(k); })) {
|
|
positions[2] = HD_HASH_4BYTE_FLAG;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
template <std::size_t N, std::size_t M>
|
|
consteval bool try_compute_phf_hd(const std::array<std::string_view, N>& keys, phf_result<N>& result) {
|
|
static_assert(M <= phf_result<N>::MAX_TABLE_SIZE, "Table size M exceeds maximum");
|
|
std::array<std::array<std::size_t, 256>, MAX_POSITIONS> asso{};
|
|
std::size_t npos{};
|
|
std::array<std::size_t, MAX_POSITIONS> pos{};
|
|
std::array<std::size_t, M> s2k{};
|
|
if (try_hash_and_displace<N, M>(keys, asso, npos, pos, s2k)) {
|
|
result.table_size = M;
|
|
result.asso_values = asso;
|
|
result.num_positions = npos;
|
|
result.positions = pos;
|
|
for (std::size_t i = 0; i < M; ++i) { result.slot_to_key[i] = s2k[i]; }
|
|
for (std::size_t i = M; i < phf_result<N>::MAX_TABLE_SIZE; ++i) { result.slot_to_key[i] = N; }
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
template <std::size_t N, std::size_t M>
|
|
consteval phf_result<N> compute_phf_hd_po2(const std::array<std::string_view, N>& keys) {
|
|
static_assert(M <= phf_result<N>::MAX_TABLE_SIZE, "Table size M exceeds maximum");
|
|
phf_result<N> result{};
|
|
if (try_compute_phf_hd<N, M>(keys, result)) { return result; }
|
|
constexpr std::size_t NextM = M * 2;
|
|
if constexpr (NextM <= phf_result<N>::MAX_TABLE_SIZE) {
|
|
return compute_phf_hd_po2<N, NextM>(keys);
|
|
} else {
|
|
throw "Hash-and-Displace: failed to find valid table size";
|
|
}
|
|
}
|
|
|
|
// Compute a perfect hash for `keys`: try gperf at power-of-two sizes (capped so
|
|
// the runtime tables stay within uint8 indices), then fall back to H&D.
|
|
template <std::size_t N>
|
|
consteval phf_result<N> compute_phf(const std::array<std::string_view, N>& keys) {
|
|
constexpr std::size_t StartM = next_power_of_2(N);
|
|
constexpr std::size_t GPERF_MAX_TABLE =
|
|
phf_result<N>::MAX_TABLE_SIZE < 256 ? phf_result<N>::MAX_TABLE_SIZE : 256;
|
|
if constexpr (StartM <= GPERF_MAX_TABLE) {
|
|
phf_result<N> result{};
|
|
if (try_gperf_po2<N, StartM, GPERF_MAX_TABLE>(keys, result)) { return result; }
|
|
}
|
|
return compute_phf_hd_po2<N, StartM>(keys);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Runtime tables (flat, uint8) derived from a phf_result.
|
|
// ============================================================================
|
|
|
|
template <std::size_t N, std::size_t TableSize, std::size_t MaxKeyLen>
|
|
struct phf_data {
|
|
std::array<std::array<std::uint8_t, 256>, MAX_POSITIONS> asso_values{};
|
|
std::array<std::uint8_t, MAX_POSITIONS> positions{};
|
|
std::uint8_t num_positions{};
|
|
std::uint8_t hd_hash_variant{}; // 2 or 4 (H&D only)
|
|
std::array<std::uint8_t, TableSize> slot_to_key{};
|
|
// slot_key_bytes[s] holds the key stored at slot s, zero-padded to a 16-byte
|
|
// multiple so the SIMD comparison can read a whole register.
|
|
std::array<std::array<char, ((MaxKeyLen + 15) / 16) * 16>, TableSize> slot_key_bytes{};
|
|
std::array<std::uint8_t, TableSize> slot_key_len{};
|
|
};
|
|
|
|
template <std::size_t N>
|
|
constexpr std::size_t compute_max_key_len(const std::array<std::string_view, N>& keys) noexcept {
|
|
std::size_t m = 0;
|
|
for (std::size_t i = 0; i < N; ++i) { if (keys[i].size() > m) { m = keys[i].size(); } }
|
|
return m;
|
|
}
|
|
|
|
// Validate keys and build the runtime tables from the computed perfect hash.
|
|
template <std::size_t N, std::size_t TableSize, std::size_t MaxKeyLen>
|
|
consteval phf_data<N, TableSize, MaxKeyLen>
|
|
build_phf_data(const std::array<std::string_view, N>& keys, const phf_result<N>& result) {
|
|
for (std::size_t i = 0; i < N; ++i) {
|
|
if (keys[i].empty()) { throw "empty keys are not allowed in key_selector"; }
|
|
if (keys[i].size() > MaxKeyLen) { throw "key length exceeds MaxKeyLen"; }
|
|
for (char c : keys[i]) {
|
|
if (c == '\\') { throw "backslash not allowed in key_selector keys"; }
|
|
if (c == '"') { throw "quote not allowed in key_selector keys"; }
|
|
if (c == '\0') { throw "null byte not allowed in key_selector keys"; }
|
|
}
|
|
for (std::size_t j = i + 1; j < N; ++j) {
|
|
if (keys[i] == keys[j]) { throw "duplicate keys in key_selector"; }
|
|
}
|
|
}
|
|
|
|
phf_data<N, TableSize, MaxKeyLen> out{};
|
|
|
|
if (result.num_positions == HD_MODE) {
|
|
// H&D mode: single displacement table in asso_values[0].
|
|
for (std::size_t c = 0; c < 256; ++c) {
|
|
out.asso_values[0][c] = static_cast<std::uint8_t>(result.asso_values[0][c]);
|
|
}
|
|
out.num_positions = static_cast<std::uint8_t>(HD_MODE);
|
|
out.hd_hash_variant = static_cast<std::uint8_t>(result.positions[2]);
|
|
} else {
|
|
for (std::size_t pi = 0; pi < result.num_positions; ++pi) {
|
|
for (std::size_t c = 0; c < 256; ++c) {
|
|
out.asso_values[pi][c] = static_cast<std::uint8_t>(result.asso_values[pi][c] % TableSize);
|
|
}
|
|
}
|
|
out.num_positions = static_cast<std::uint8_t>(result.num_positions);
|
|
for (std::size_t i = 0; i < result.num_positions; ++i) {
|
|
out.positions[i] = (result.positions[i] == LAST_CHAR)
|
|
? POS_LAST_CHAR
|
|
: static_cast<std::uint8_t>(result.positions[i]);
|
|
}
|
|
}
|
|
|
|
for (std::size_t s = 0; s < TableSize; ++s) {
|
|
out.slot_to_key[s] = static_cast<std::uint8_t>(result.slot_to_key[s]);
|
|
}
|
|
|
|
for (std::size_t s = 0; s < TableSize; ++s) {
|
|
std::size_t ki = result.slot_to_key[s];
|
|
if (ki < N) {
|
|
auto k = keys[ki];
|
|
out.slot_key_len[s] = static_cast<std::uint8_t>(k.size());
|
|
for (std::size_t c = 0; c < k.size(); ++c) { out.slot_key_bytes[s][c] = k[c]; }
|
|
} else {
|
|
out.slot_key_len[s] = 0; // empty slot: no length can match
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// --- SIMD runtime primitives ------------------------------------------------
|
|
|
|
// Scan for the terminating '"' starting at p. Returns its byte offset (= key
|
|
// length). Caller guarantees SIMDJSON_PADDING bytes past the JSON buffer, so the
|
|
// load is safe.
|
|
template <std::size_t MaxKeyLen>
|
|
simdjson_really_inline std::size_t scan_key_length(const char* p) noexcept {
|
|
// The SIMD paths scan only two 16-byte blocks (offsets 0..31), so a key
|
|
// whose closing quote sits at offset 32 would be missed. Cap at 31 to keep
|
|
// SIMD and scalar builds in agreement.
|
|
static_assert(MaxKeyLen <= 31, "MaxKeyLen must be <= 31 for current SIMD implementations");
|
|
#if SIMDJSON_KEY_SELECTOR_HAS_NEON
|
|
uint8x16_t v0 = vld1q_u8(reinterpret_cast<const uint8_t*>(p));
|
|
uint8x16_t cmp0 = vceqq_u8(v0, vdupq_n_u8('"'));
|
|
uint64_t m0 = vget_lane_u64(
|
|
vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(cmp0), 4)), 0);
|
|
if constexpr (MaxKeyLen < 16) {
|
|
if (simdjson_likely(m0 != 0)) return std::size_t(__builtin_ctzll(m0)) >> 2;
|
|
return MaxKeyLen + 1;
|
|
} else {
|
|
uint8x16_t v1 = vld1q_u8(reinterpret_cast<const uint8_t*>(p) + 16);
|
|
uint8x16_t cmp1 = vceqq_u8(v1, vdupq_n_u8('"'));
|
|
uint64_t m1 = vget_lane_u64(
|
|
vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(cmp1), 4)), 0);
|
|
if (simdjson_likely(m0 != 0)) return std::size_t(__builtin_ctzll(m0)) >> 2;
|
|
if (m1 != 0) return 16 + (std::size_t(__builtin_ctzll(m1)) >> 2);
|
|
return MaxKeyLen + 1;
|
|
}
|
|
#elif SIMDJSON_KEY_SELECTOR_HAS_SSE2
|
|
__m128i v0 = _mm_loadu_si128(reinterpret_cast<const __m128i*>(p));
|
|
__m128i cmp0 = _mm_cmpeq_epi8(v0, _mm_set1_epi8('"'));
|
|
unsigned m0 = static_cast<unsigned>(_mm_movemask_epi8(cmp0));
|
|
if constexpr (MaxKeyLen < 16) {
|
|
if (simdjson_likely(m0 != 0)) return std::size_t(__builtin_ctz(m0));
|
|
return MaxKeyLen + 1;
|
|
} else {
|
|
__m128i v1 = _mm_loadu_si128(reinterpret_cast<const __m128i*>(p + 16));
|
|
__m128i cmp1 = _mm_cmpeq_epi8(v1, _mm_set1_epi8('"'));
|
|
unsigned m1 = static_cast<unsigned>(_mm_movemask_epi8(cmp1));
|
|
if (simdjson_likely(m0 != 0)) return std::size_t(__builtin_ctz(m0));
|
|
if (m1 != 0) return 16 + std::size_t(__builtin_ctz(m1));
|
|
return MaxKeyLen + 1;
|
|
}
|
|
#else
|
|
for (std::size_t i = 0; i <= MaxKeyLen; ++i)
|
|
if (p[i] == '"') return i;
|
|
return MaxKeyLen + 1;
|
|
#endif
|
|
}
|
|
|
|
// Byte-equal of p[0..len) against stored[0..len). stored is zero-padded past
|
|
// `len`. Input is read over 16 or 32 bytes (padded JSON buffer guaranteed).
|
|
template <std::size_t MaxKeyLen>
|
|
simdjson_really_inline bool compare_key_bytes(
|
|
const char* p, const char* stored, std::size_t len) noexcept {
|
|
alignas(16) static constexpr uint8_t idx16[16] =
|
|
{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};
|
|
if constexpr (MaxKeyLen <= 16) {
|
|
#if SIMDJSON_KEY_SELECTOR_HAS_NEON
|
|
uint8x16_t vp = vld1q_u8(reinterpret_cast<const uint8_t*>(p));
|
|
uint8x16_t vs = vld1q_u8(reinterpret_cast<const uint8_t*>(stored));
|
|
uint8x16_t mask = vcltq_u8(vld1q_u8(idx16), vdupq_n_u8(static_cast<uint8_t>(len)));
|
|
uint8x16_t diff = veorq_u8(vandq_u8(vp, mask), vs);
|
|
// A 32-bit-lane horizontal max is enough to decide "all bytes equal"
|
|
// (diff is zero iff every 32-bit word is zero) and is cheaper than a
|
|
// byte-wide reduction.
|
|
return vmaxvq_u32(vreinterpretq_u32_u8(diff)) == 0;
|
|
#elif SIMDJSON_KEY_SELECTOR_HAS_SSE2
|
|
__m128i vp = _mm_loadu_si128(reinterpret_cast<const __m128i*>(p));
|
|
__m128i vs = _mm_loadu_si128(reinterpret_cast<const __m128i*>(stored));
|
|
__m128i idx = _mm_load_si128(reinterpret_cast<const __m128i*>(idx16));
|
|
__m128i mask = _mm_cmplt_epi8(idx, _mm_set1_epi8(static_cast<char>(len)));
|
|
__m128i eq = _mm_cmpeq_epi8(_mm_and_si128(vp, mask), vs);
|
|
return _mm_movemask_epi8(eq) == 0xFFFF;
|
|
#else
|
|
for (std::size_t i = 0; i < len; ++i)
|
|
if (p[i] != stored[i]) return false;
|
|
return true;
|
|
#endif
|
|
} else if constexpr (MaxKeyLen <= 32) {
|
|
alignas(16) static constexpr uint8_t idx32_hi[16] =
|
|
{16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31};
|
|
#if SIMDJSON_KEY_SELECTOR_HAS_NEON
|
|
uint8x16_t vp_lo = vld1q_u8(reinterpret_cast<const uint8_t*>(p));
|
|
uint8x16_t vp_hi = vld1q_u8(reinterpret_cast<const uint8_t*>(p) + 16);
|
|
uint8x16_t vs_lo = vld1q_u8(reinterpret_cast<const uint8_t*>(stored));
|
|
uint8x16_t vs_hi = vld1q_u8(reinterpret_cast<const uint8_t*>(stored) + 16);
|
|
uint8x16_t lenv = vdupq_n_u8(static_cast<uint8_t>(len));
|
|
uint8x16_t m_lo = vcltq_u8(vld1q_u8(idx16), lenv);
|
|
uint8x16_t m_hi = vcltq_u8(vld1q_u8(idx32_hi), lenv);
|
|
uint8x16_t d_lo = veorq_u8(vandq_u8(vp_lo, m_lo), vs_lo);
|
|
uint8x16_t d_hi = veorq_u8(vandq_u8(vp_hi, m_hi), vs_hi);
|
|
return vmaxvq_u32(vreinterpretq_u32_u8(vorrq_u8(d_lo, d_hi))) == 0;
|
|
#elif SIMDJSON_KEY_SELECTOR_HAS_SSE2
|
|
__m128i vp_lo = _mm_loadu_si128(reinterpret_cast<const __m128i*>(p));
|
|
__m128i vp_hi = _mm_loadu_si128(reinterpret_cast<const __m128i*>(p + 16));
|
|
__m128i vs_lo = _mm_loadu_si128(reinterpret_cast<const __m128i*>(stored));
|
|
__m128i vs_hi = _mm_loadu_si128(reinterpret_cast<const __m128i*>(stored + 16));
|
|
__m128i lenv = _mm_set1_epi8(static_cast<char>(len));
|
|
__m128i m_lo = _mm_cmplt_epi8(_mm_load_si128(reinterpret_cast<const __m128i*>(idx16)), lenv);
|
|
__m128i m_hi = _mm_cmplt_epi8(_mm_load_si128(reinterpret_cast<const __m128i*>(idx32_hi)), lenv);
|
|
__m128i eq_lo = _mm_cmpeq_epi8(_mm_and_si128(vp_lo, m_lo), vs_lo);
|
|
__m128i eq_hi = _mm_cmpeq_epi8(_mm_and_si128(vp_hi, m_hi), vs_hi);
|
|
return (_mm_movemask_epi8(eq_lo) & _mm_movemask_epi8(eq_hi)) == 0xFFFF;
|
|
#else
|
|
for (std::size_t i = 0; i < len; ++i)
|
|
if (p[i] != stored[i]) return false;
|
|
return true;
|
|
#endif
|
|
} else {
|
|
for (std::size_t i = 0; i < len; ++i)
|
|
if (p[i] != stored[i]) return false;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
} // namespace key_selector_detail
|
|
|
|
/**
|
|
* Stateless, compile-time key selector.
|
|
*
|
|
* Usage:
|
|
* using sel_t = key_selector<"id", "text", "user">;
|
|
* std::size_t i = sel_t::match_raw(raw_key); // returns sel_t::size() on miss
|
|
*
|
|
* The perfect hash is built at compile time (gperf-style, with a
|
|
* Hash-and-Displace fallback) and only flat tables survive to runtime. All
|
|
* tables are static constexpr, so the lookup fully inlines.
|
|
*
|
|
* Limitations:
|
|
* - Each key must be at most 31 characters long (and no longer than
|
|
* SIMDJSON_PADDING). Longer keys trigger a compile-time error.
|
|
* - The number of keys should be moderate. The hard limit is 255 keys;
|
|
* compilation time grows with the number of keys, so prefer a few dozen at
|
|
* most per selector.
|
|
* - Keys must be distinct, non-empty, and free of backslash, double-quote and
|
|
* null bytes (matching is done against the raw, unescaped JSON key bytes).
|
|
*/
|
|
template <constevalutil::fixed_string... Keys>
|
|
struct key_selector {
|
|
static constexpr std::size_t N = sizeof...(Keys);
|
|
static_assert(N > 0, "key_selector requires at least one key");
|
|
static_assert(N <= 255,"key_selector supports at most 255 keys");
|
|
|
|
static constexpr std::array<std::string_view, N> keys{ Keys.view()... };
|
|
static constexpr std::size_t max_key_len = key_selector_detail::compute_max_key_len<N>(keys);
|
|
static_assert(max_key_len <= SIMDJSON_PADDING,
|
|
"key longer than SIMDJSON_PADDING is not supported");
|
|
// The SIMD key-length scan covers offsets 0..31 only; a 32-character key's
|
|
// closing quote lands at offset 32 and would be silently missed on
|
|
// NEON/SSE2 while still matching in scalar builds. Cap at 31 so the result
|
|
// is identical across implementations.
|
|
static_assert(max_key_len <= 31,
|
|
"key_selector keys must be at most 31 characters long");
|
|
|
|
static constexpr auto result = key_selector_detail::compute_phf<N>(keys);
|
|
static constexpr std::size_t table_size = result.table_size;
|
|
|
|
static constexpr auto phf =
|
|
key_selector_detail::build_phf_data<N, table_size, max_key_len>(keys, result);
|
|
|
|
static constexpr std::size_t size() noexcept { return N; }
|
|
|
|
/**
|
|
* Look up a JSON key. rjs must point just after an opening quote in a padded
|
|
* simdjson buffer. Returns the selector index in [0, N) on match, or N on miss.
|
|
*/
|
|
static simdjson_really_inline std::size_t match_raw(raw_json_string rjs) noexcept {
|
|
const char* p = rjs.raw();
|
|
std::size_t len = key_selector_detail::scan_key_length<max_key_len>(p);
|
|
if (len == 0 || len > max_key_len) { return N; }
|
|
|
|
std::size_t slot;
|
|
if (phf.num_positions == key_selector_detail::HD_MODE) {
|
|
// Hash-and-Displace: bucket displacement + per-key hash.
|
|
std::string_view key(p, len);
|
|
std::size_t bucket = key_selector_detail::hd_bucket_hash(key);
|
|
std::size_t kh = (phf.hd_hash_variant == key_selector_detail::HD_HASH_2BYTE_FLAG)
|
|
? key_selector_detail::hd_key_hash_2(key)
|
|
: key_selector_detail::hd_key_hash_4(key);
|
|
slot = (phf.asso_values[0][bucket] + kh) & (table_size - 1);
|
|
} else {
|
|
// gperf: h = len + sum of asso_values over the selected positions.
|
|
// positions / num_positions / asso_values are compile-time constants,
|
|
// so this loop fully unrolls. The idx < len guard mirrors the
|
|
// generator's char_at()-> 256 -> skip behavior for out-of-range
|
|
// positions (required: arbitrary positions may exceed a key's length).
|
|
std::size_t h = len;
|
|
for (std::uint8_t i = 0; i < phf.num_positions; ++i) {
|
|
std::uint8_t pos = phf.positions[i];
|
|
std::size_t idx = (pos == key_selector_detail::POS_LAST_CHAR)
|
|
? (len - std::size_t{1})
|
|
: static_cast<std::size_t>(pos);
|
|
if (idx < len) {
|
|
h += phf.asso_values[i][static_cast<unsigned char>(p[idx])];
|
|
}
|
|
}
|
|
slot = h & (table_size - 1);
|
|
}
|
|
|
|
std::uint8_t ki = phf.slot_to_key[slot];
|
|
if (ki >= N) { return N; }
|
|
if (phf.slot_key_len[slot] != len) { return N; }
|
|
if (!key_selector_detail::compare_key_bytes<max_key_len>(
|
|
p, phf.slot_key_bytes[slot].data(), len)) { return N; }
|
|
return ki;
|
|
}
|
|
|
|
/** Return the key text at selector index i (i in [0, N)). */
|
|
static constexpr std::string_view key_at(std::size_t i) noexcept {
|
|
return keys[i];
|
|
}
|
|
};
|
|
|
|
} // namespace ondemand
|
|
} // namespace SIMDJSON_IMPLEMENTATION
|
|
} // namespace simdjson
|
|
|
|
#endif // SIMDJSON_SUPPORTS_CONCEPTS
|
|
|
|
#endif // SIMDJSON_GENERIC_ONDEMAND_KEY_SELECTOR_H
|