Skip to content

Commit 49a2a87

Browse files
Using Radix tree instead of Trie (#40160)
## Using Radix Tree instead of Trie ### Benchmarking Command - _Both the commands have been run on same machine_ ``` bazel run -c opt test/common/common:trie_lookup_table_speed_test -- --benchmark_repetitions=10 bazel run -c opt test/common/common:radix_tree_speed_test -- --benchmark_repetitions=10 ``` #### Lookups | Benchmark | Trie Mean | RadixTree Mean | % Diff (RadixTree vs. Trie) | | --------------------------------------- | --------- | -------------- | --------------------------------- | | bmLookupsRequestHeaders_mean | 82.7 ns | 37.2 ns | ⚡ 54.99% faster | | bmLookupsResponseHeaders_mean | 74.3 ns | 19.8 ns | ⚡ 73.35% faster | | bmLookups/10/0_mean | 299.0 ns | 10.4 ns | ⚡ 96.52% faster | | bmLookups/100/0_mean | 365.0 ns | 23.5 ns | ⚡ 93.56% faster | | bmLookups/1000/0_mean | 447.0 ns | 39.2 ns | ⚡ 91.23% faster | | bmLookups/10000/0_mean | 541.0 ns | 74.5 ns | ⚡ 86.23% faster | | bmLookups/10/8_mean | 22.3 ns | 14.3 ns | ⚡ 35.87% faster | | bmLookups/100/8_mean | 24.5 ns | 24.0 ns | ⚡ 2.04% faster | | bmLookups/1000/8_mean | 35.3 ns | 35.8 ns | 🐢 1.42% slower | | bmLookups/10000/8_mean | 74.1 ns | 56.0 ns | ⚡ 24.43% faster | | bmLookups/10/128_mean | 631.0 ns | 22.2 ns | ⚡ 96.48% faster | | bmLookups/100/128_mean | 764.0 ns | 23.8 ns | ⚡ 96.88% faster | | bmLookups/1000/128_mean | 799.0 ns | 43.1 ns | ⚡ 94.61% faster | | bmLookups/10000/128_mean | 1080.0 ns | 76.4 ns | ⚡ 92.93% faster | #### findMatchingPrefixes | Benchmark Name | TrieLookupTable (ns) | RadixTree (ns) | Performance | | ---------------------------------- | -------------------- | -------------- | ---------------- | | PrefixMatching/100/3/1000_mean | 1955 | 1740 | 10.99% Faster ⚡️ | | PrefixMatching/100/5/1000_mean | 1907 | 1764 | 7.50% Faster ⚡️ | | PrefixMatching/100/8/1000_mean | 1860 | 1812 | 2.58% Faster ⚡️ | | PrefixMatching/1000/3/1000_mean | 2480 | 2146 | 13.47% Faster ⚡️ | | PrefixMatching/1000/5/1000_mean | 2265 | 2146 | 5.25% Faster ⚡️ | | PrefixMatching/1000/8/1000_mean | 2551 | 2146 | 15.88% Faster ⚡️ | | PrefixMatching/10000/3/1000_mean | 3266 | 3004 | 8.02% Faster ⚡️ | | PrefixMatching/10000/5/1000_mean | 4005 | 3338 | 16.65% Faster ⚡️ | | PrefixMatching/10000/8/1000_mean | 6533 | 4435 | 32.11% Faster ⚡️ | | RequestHeadersPrefixMatching_mean | 1788 | 1764 | 1.34% Faster ⚡️ | | ResponseHeadersPrefixMatching_mean | 1597 | 1645 | 3.01% Slower 🐢 | Commit Message: Using Radix Tree instead of Trie Additional Description: Risk Level: Testing: Docs Changes: Release Notes: Platform Specific Features: [Optional Runtime guard:] [Optional Fixes #Issue] [Optional Fixes commit #PR or SHA] [Optional Deprecated:] [Optional [API Considerations](https://github.com/envoyproxy/envoy/blob/main/api/review_checklist.md):] --------- Signed-off-by: Ashesh Vidyut <[email protected]> Signed-off-by: Rohit Agrawal <[email protected]> Signed-off-by: Ashesh Vidyut <[email protected]> Co-authored-by: Rohit Agrawal <[email protected]>
1 parent ad6af72 commit 49a2a87

File tree

14 files changed

+979
-13
lines changed

14 files changed

+979
-13
lines changed

changelogs/current.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,9 @@ new_features:
387387
- area: alts
388388
change: |
389389
Added environment variable-protected gRPC keepalive params to the ALTS handshaker client.
390+
- area: dns_filter, redis_proxy and prefix_matcher_map
391+
change: |
392+
Using Radix Tree instead of Trie for performance improvements.
390393
- area: wasm
391394
change: |
392395
Added support for returning ``StopIteration`` from plugin ``onRequestHeader`` and ``onResponseHeader`` callbacks. See

source/common/common/BUILD

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,16 @@ envoy_cc_library(
483483
hdrs = ["trie_lookup_table.h"],
484484
)
485485

486+
envoy_cc_library(
487+
name = "radix_tree_lib",
488+
hdrs = ["radix_tree.h"],
489+
deps = [
490+
":assert_lib",
491+
"@com_google_absl//absl/container:flat_hash_map",
492+
"@com_google_absl//absl/strings",
493+
],
494+
)
495+
486496
envoy_cc_library(
487497
name = "utility_lib",
488498
srcs = ["utility.cc"],

source/common/common/radix_tree.h

Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
#pragma once
2+
3+
#include <algorithm>
4+
#include <vector>
5+
6+
#include "envoy/common/optref.h"
7+
8+
#include "source/common/common/assert.h"
9+
10+
#include "absl/container/flat_hash_map.h"
11+
#include "absl/strings/string_view.h"
12+
#include "absl/types/optional.h"
13+
14+
namespace Envoy {
15+
template <class Value> class RadixTree {
16+
static constexpr int32_t NoNode = -1;
17+
struct RadixTreeNode {
18+
std::string prefix_;
19+
Value value_{};
20+
// Hash map for O(1) child lookup by first character
21+
absl::flat_hash_map<uint8_t, RadixTreeNode> children_;
22+
23+
/**
24+
* Insert a key-value pair into this node
25+
* @param key the full key being inserted
26+
* @param search the remaining search key
27+
* @param value the value to insert
28+
*/
29+
void insert(absl::string_view search, Value value) {
30+
// Handle key exhaustion
31+
if (search.empty()) {
32+
value_ = std::move(value);
33+
return;
34+
}
35+
36+
// Look for the edge
37+
uint8_t firstChar = static_cast<uint8_t>(search[0]);
38+
auto childIt = children_.find(firstChar);
39+
40+
// No edge, create one
41+
if (childIt == children_.end()) {
42+
// Create a new child node
43+
RadixTreeNode newChild;
44+
newChild.prefix_ = std::string(search);
45+
newChild.value_ = std::move(value);
46+
47+
// Add the child to the current node
48+
children_[firstChar] = std::move(newChild);
49+
return;
50+
}
51+
52+
// Get the child node
53+
RadixTreeNode& child = childIt->second;
54+
55+
// Determine longest prefix length of the search key on match
56+
size_t cpl = commonPrefixLength(search, child.prefix_);
57+
if (cpl == child.prefix_.size()) {
58+
// The search key is longer than the child prefix, continue down
59+
absl::string_view remaining_search = search.substr(cpl);
60+
child.insert(remaining_search, std::move(value));
61+
return;
62+
}
63+
64+
// Split the node - create a new intermediate node
65+
RadixTreeNode split_node;
66+
split_node.prefix_ = std::string(search.substr(0, cpl));
67+
68+
// Update the child's prefix
69+
child.prefix_ = std::string(child.prefix_.substr(cpl));
70+
71+
// If the search key is exactly the common prefix, set the value on the split node
72+
if (cpl == search.size()) {
73+
split_node.value_ = std::move(value);
74+
} else {
75+
// Create a new leaf for the current key
76+
RadixTreeNode new_leaf;
77+
new_leaf.prefix_ = std::string(search.substr(cpl));
78+
new_leaf.value_ = std::move(value);
79+
split_node.children_[static_cast<uint8_t>(new_leaf.prefix_[0])] = std::move(new_leaf);
80+
}
81+
82+
// Add the child to the split node
83+
split_node.children_[static_cast<uint8_t>(child.prefix_[0])] = std::move(child);
84+
85+
// Replace the original child with the split node
86+
children_[firstChar] = std::move(split_node);
87+
}
88+
89+
/**
90+
* Recursive helper for find operation.
91+
* @param search the remaining search key.
92+
* @param result the value to return if found.
93+
* @return true if the key was found, false otherwise.
94+
*/
95+
bool findRecursive(absl::string_view search, Value& result) const {
96+
if (search.empty()) {
97+
if (has_value(*this)) {
98+
result = value_;
99+
return true;
100+
}
101+
return false;
102+
}
103+
104+
uint8_t firstChar = static_cast<uint8_t>(search[0]);
105+
auto childIt = children_.find(firstChar);
106+
if (childIt == children_.end()) {
107+
return false;
108+
}
109+
110+
const RadixTreeNode& child = childIt->second;
111+
112+
// Check if the child's prefix matches the search
113+
if (search.size() >= child.prefix_.size() &&
114+
search.substr(0, child.prefix_.size()) == child.prefix_) {
115+
absl::string_view new_search = search.substr(child.prefix_.size());
116+
return child.findRecursive(new_search, result);
117+
}
118+
119+
return false;
120+
}
121+
122+
/**
123+
* Get a child node by character key
124+
*/
125+
Envoy::OptRef<const RadixTreeNode> getChild(uint8_t char_key) const {
126+
auto it = children_.find(char_key);
127+
if (it != children_.end()) {
128+
return {it->second};
129+
}
130+
return {};
131+
}
132+
};
133+
134+
/**
135+
* Check if a node has a value (is a leaf node)
136+
*/
137+
static bool has_value(const RadixTreeNode& node) {
138+
// For pointer types, check if the pointer is not null
139+
if constexpr (std::is_pointer_v<Value>) {
140+
return node.value_ != nullptr;
141+
} else {
142+
return static_cast<bool>(node.value_);
143+
}
144+
}
145+
146+
/**
147+
* Find the longest common prefix between two strings
148+
*/
149+
static size_t commonPrefixLength(absl::string_view a, absl::string_view b) {
150+
size_t len = std::min(a.size(), b.size());
151+
for (size_t i = 0; i < len; i++) {
152+
if (a[i] != b[i]) {
153+
return i;
154+
}
155+
}
156+
return len;
157+
}
158+
159+
public:
160+
/**
161+
* Adds an entry to the RadixTree at the given Key.
162+
* @param key the key used to add the entry.
163+
* @param value the value to be associated with the key.
164+
* @param overwrite_existing will overwrite the value when the value for a given key already
165+
* exists.
166+
* @return false when a value already exists for the given key.
167+
*/
168+
bool add(absl::string_view key, Value value, bool overwrite_existing = true) {
169+
// Check if the key already exists
170+
Value existing;
171+
bool found = root_.findRecursive(key, existing);
172+
173+
// If a value exists and we shouldn't overwrite, return false
174+
if (found && !overwrite_existing) {
175+
return false;
176+
}
177+
178+
root_.insert(key, std::move(value));
179+
return true;
180+
}
181+
182+
/**
183+
* Finds the entry associated with the key.
184+
* @param key the key used to find.
185+
* @return the Value associated with the key, or an empty-initialized Value
186+
* if there is no matching key.
187+
*/
188+
Value find(absl::string_view key) const {
189+
Value result;
190+
if (root_.findRecursive(key, result)) {
191+
return result;
192+
}
193+
return Value{};
194+
}
195+
196+
/**
197+
* Returns the set of entries that are prefixes of the specified key, longest last.
198+
* Complexity is O(min(longest key prefix, key length)).
199+
* @param key the key used to find.
200+
* @return a vector of values whose keys are a prefix of the specified key, longest last.
201+
*/
202+
absl::InlinedVector<Value, 4> findMatchingPrefixes(absl::string_view key) const {
203+
absl::InlinedVector<Value, 4> result;
204+
absl::string_view search = key;
205+
const RadixTreeNode* node = &root_;
206+
207+
// Special case: if searching for empty string, check root node
208+
if (search.empty()) {
209+
if (has_value(*node)) {
210+
result.push_back(node->value_);
211+
}
212+
return result;
213+
}
214+
215+
while (true) {
216+
// Check if current node has a value (is a leaf) and we've consumed some prefix
217+
if (has_value(*node)) {
218+
result.push_back(node->value_);
219+
}
220+
221+
// Check for key exhaustion
222+
if (search.empty()) {
223+
break;
224+
}
225+
226+
// Look for an edge
227+
uint8_t firstChar = static_cast<uint8_t>(search[0]);
228+
auto child = node->getChild(firstChar);
229+
if (!child) {
230+
break;
231+
}
232+
233+
const RadixTreeNode& child_node = *child;
234+
node = &child_node;
235+
236+
// Consume the search prefix
237+
if (search.size() < child->prefix_.size() ||
238+
search.substr(0, child->prefix_.size()) != child->prefix_) {
239+
break;
240+
}
241+
// Consume the search prefix
242+
search = search.substr(child->prefix_.size());
243+
}
244+
245+
return result;
246+
}
247+
248+
/**
249+
* Finds the entry with the longest key that is a prefix of the specified key.
250+
* Complexity is O(min(longest key prefix, key length)).
251+
* @param key the key used to find.
252+
* @return a value whose key is a prefix of the specified key. If there are
253+
* multiple such values, the one with the longest key. If there are
254+
* no keys that are a prefix of the input key, an empty-initialized Value.
255+
*/
256+
Value findLongestPrefix(absl::string_view key) const {
257+
absl::string_view search = key;
258+
const RadixTreeNode* node = &root_;
259+
const RadixTreeNode* last_node_with_value = nullptr;
260+
261+
while (true) {
262+
// Look for a leaf node
263+
if (has_value(*node)) {
264+
last_node_with_value = node;
265+
}
266+
267+
// Check for key exhaustion
268+
if (search.empty()) {
269+
break;
270+
}
271+
272+
// Look for an edge
273+
uint8_t firstChar = static_cast<uint8_t>(search[0]);
274+
auto child = node->getChild(firstChar);
275+
if (!child) {
276+
break;
277+
}
278+
279+
const RadixTreeNode& child_node = *child;
280+
node = &child_node;
281+
282+
// Consume the search prefix
283+
if (search.size() < child->prefix_.size() ||
284+
search.substr(0, child->prefix_.size()) != child->prefix_) {
285+
break;
286+
}
287+
// Consume the search prefix
288+
search = search.substr(child->prefix_.size());
289+
}
290+
291+
// Return the value from the last node that had a value, or empty value if none found
292+
if (last_node_with_value != nullptr) {
293+
return last_node_with_value->value_;
294+
}
295+
return nullptr;
296+
}
297+
298+
private:
299+
// Initialized with a single empty node as the root node.
300+
RadixTreeNode root_ = RadixTreeNode();
301+
};
302+
} // namespace Envoy

source/common/matcher/BUILD

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ envoy_cc_library(
2929
hdrs = ["prefix_map_matcher.h"],
3030
deps = [
3131
":map_matcher_lib",
32-
"//source/common/common:trie_lookup_table_lib",
32+
"//source/common/common:radix_tree_lib",
3333
"//source/common/runtime:runtime_features_lib",
3434
],
3535
)

source/common/matcher/prefix_map_matcher.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#pragma once
22

3-
#include "source/common/common/trie_lookup_table.h"
3+
#include "source/common/common/radix_tree.h"
44
#include "source/common/matcher/map_matcher.h"
55
#include "source/common/runtime/runtime_features.h"
66

@@ -52,7 +52,7 @@ template <class DataType> class PrefixMapMatcher : public MapMatcher<DataType> {
5252
}
5353

5454
private:
55-
TrieLookupTable<std::shared_ptr<OnMatch<DataType>>> children_;
55+
RadixTree<std::shared_ptr<OnMatch<DataType>>> children_;
5656
};
5757

5858
} // namespace Matcher

source/extensions/filters/network/redis_proxy/command_splitter_impl.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
#include "envoy/stats/timespan.h"
1010

1111
#include "source/common/common/logger.h"
12-
#include "source/common/common/trie_lookup_table.h"
12+
#include "source/common/common/radix_tree.h"
1313
#include "source/common/stats/timespan_impl.h"
1414
#include "source/extensions/filters/network/common/redis/client_impl.h"
1515
#include "source/extensions/filters/network/common/redis/fault_impl.h"
@@ -501,7 +501,7 @@ class InstanceImpl : public Instance, Logger::Loggable<Logger::Id::redis> {
501501
CommandHandlerFactory<RoleRequest> role_handler_;
502502
CommandHandlerFactory<SplitKeysSumResultRequest> split_keys_sum_result_handler_;
503503
CommandHandlerFactory<TransactionRequest> transaction_handler_;
504-
TrieLookupTable<HandlerDataPtr> handler_lookup_table_;
504+
RadixTree<HandlerDataPtr> handler_lookup_table_;
505505
InstanceStats stats_;
506506
TimeSource& time_source_;
507507
Common::Redis::FaultManagerPtr fault_manager_;

source/extensions/filters/network/redis_proxy/router_impl.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
#include "envoy/type/v3/percent.pb.h"
1414
#include "envoy/upstream/cluster_manager.h"
1515

16-
#include "source/common/common/trie_lookup_table.h"
16+
#include "source/common/common/radix_tree.h"
1717
#include "source/common/http/header_map_impl.h"
1818
#include "source/common/stream_info/stream_info_impl.h"
1919
#include "source/extensions/filters/network/common/redis/supported_commands.h"
@@ -86,7 +86,7 @@ class PrefixRoutes : public Router, public Logger::Loggable<Logger::Id::redis> {
8686
const StreamInfo::StreamInfo& stream_info);
8787

8888
private:
89-
TrieLookupTable<PrefixSharedPtr> prefix_lookup_table_;
89+
RadixTree<PrefixSharedPtr> prefix_lookup_table_;
9090
const bool case_insensitive_;
9191
Upstreams upstreams_;
9292
PrefixSharedPtr catch_all_route_;

0 commit comments

Comments
 (0)