From e37c41ae4e2726fa4998965cbded4d5103f54e83 Mon Sep 17 00:00:00 2001 From: yhirose Date: Sun, 12 Jul 2026 22:45:31 -0400 Subject: [PATCH] Speed up lookup by ~13% with template callbacks and a root dispatch table Two format-neutral changes to the read path (the byte code is unchanged and stays fully compatible): - matcher::match takes its output/prefix callbacks as template parameters instead of std::function, so no std::function is constructed per query and the calls inline. exact/common-prefix search on /usr/share/dict/words drop ~13%. - Each matcher precomputes a 256-entry label->address dispatch table for the root state at construction time, replacing the root's jump table binary search (every query passes through the root) with an O(1) lookup. Costs 1KB of memory per matcher; the file is untouched. --- fstlib.h | 73 +++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 62 insertions(+), 11 deletions(-) diff --git a/fstlib.h b/fstlib.h index 166ade7..6c6d157 100644 --- a/fstlib.h +++ b/fstlib.h @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -1950,6 +1951,8 @@ template class matcher { } is_valid_ = true; + + build_root_dispatch(); } operator bool() const { return is_valid_; } @@ -1961,10 +1964,14 @@ template class matcher { } protected: - bool match( - const char *str, size_t len, - std::function outputs = nullptr, - std::function prefixes = nullptr) const { + // The callbacks are template parameters (with std::nullptr_t defaults), so + // no std::function is constructed per query and the calls are inlined. + template + bool match(const char *str, size_t len, OutputsFn outputs = nullptr, + PrefixesFn prefixes = nullptr) const { + constexpr auto has_outputs = !std::is_null_pointer_v; + constexpr auto has_prefixes = !std::is_null_pointer_v; if (trace_) { std::cout << "Char\tAddress\tArc\tN F L\tNxtAddr\tOutput\tStOuts\tSize" @@ -1979,6 +1986,16 @@ template class matcher { auto address = header_.start_address; auto i = 0u; auto arc_in_jump_table = false; + + // Every query passes through the root state; resolve the first + // character with the precomputed dispatch table instead of the root's + // jump table binary search. + if (has_root_dispatch_ && len > 0) { + address = root_dispatch_[static_cast(str[0])]; + if (!address) { return false; } + arc_in_jump_table = true; + } + while (i < len) { auto ch = static_cast(str[i]); auto state_output = output_t{}; @@ -2116,7 +2133,7 @@ template class matcher { output += output_suffix; i++; if (ope.data.final) { - if (prefixes) { + if constexpr (has_prefixes) { if (OutputTraits::empty(state_output)) { prefixes(i, output); } else { @@ -2125,7 +2142,7 @@ template class matcher { ret = true; } if (i == len) { - if (outputs) { + if constexpr (has_outputs) { if (OutputTraits::empty(state_output)) { outputs(output); } else { @@ -2287,6 +2304,38 @@ template class matcher { } } + // If the root state has a jump table with labels, precompute a direct + // 256 entry 'label -> record address' table for it. The byte code is + // not affected; this only trades 1KB of memory per matcher for the + // root's binary search on every query. + void build_root_dispatch() { + auto address = header_.start_address; + auto p = byte_code_ + address; + + auto ope = FstOpe(*p--); + if (!ope.has_jump_table() || !header_.flags.data.jump_table_labels) { + return; + } + + auto element_size = ope.jump_table_element_size(); + size_t count = 0; + auto vb_len = vb_decode_value_reverse(p, count); + p -= vb_len; + p -= count * element_size; + + auto jump_table = p; + auto labels = reinterpret_cast(p) + 1 - count; + auto jump_table_byte_size = 1 + vb_len + count * element_size + count; + + root_dispatch_.fill(0); + for (size_t i = 0; i < count; i++) { + auto offset = lookup_jump_table(jump_table, i, element_size); + root_dispatch_[labels[i]] = + static_cast(address - (offset + jump_table_byte_size)); + } + has_root_dispatch_ = true; + } + const char *byte_code_; const size_t byte_code_size_; @@ -2294,6 +2343,9 @@ template class matcher { bool is_valid_ = false; bool trace_ = false; + bool has_root_dispatch_ = false; + std::array root_dispatch_{}; + // Suggestion template decltype(auto) suggest_core(std::string_view word, const T &matcher) const { @@ -2493,9 +2545,8 @@ template class map : public matcher { [&](const auto &_) { output = _; }); } - bool common_prefix_search( - std::string_view sv, - std::function prefixes) const { + template + bool common_prefix_search(std::string_view sv, PrefixesFn prefixes) const { return matcher::match(sv.data(), sv.size(), nullptr, prefixes); } @@ -2590,8 +2641,8 @@ class set : public matcher { static const bool has_output = false; - bool common_prefix_search(std::string_view sv, - std::function prefixes) const { + template + bool common_prefix_search(std::string_view sv, PrefixesFn prefixes) const { return matcher::match( sv.data(), sv.size(), nullptr, [&](size_t len, const none_t &) { prefixes(len); });