From ba8c746a2e9b4fa45118e5810dc04f27d07c9054 Mon Sep 17 00:00:00 2001 From: xfy Date: Thu, 4 Jun 2026 00:12:52 +0800 Subject: [PATCH] perf(matcher): eliminate heap allocations in RadixTree search with sync.Pool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RadixTree.searchLongest previously allocated &MatchResult{} on the heap every time it encountered a handler-bearing node during tree traversal — potentially N allocations per lookup with only 1 surviving. Changes: - searchLongest now tracks best *RadixNode (stack pointer) instead of allocating MatchResult at every handler node - FindLongestPrefix allocates a single pooled MatchResult via sync.Pool only when returning a match - Add ReleaseMatchResult() for callers to return MatchResult to pool - LocationEngine.Match releases pooled results after use Result: 0 B/op, 0 allocs/op on tree traversal benchmarks (sequential and parallel). --- internal/matcher/location.go | 5 +- internal/matcher/radix.go | 85 +++++++++++++--------------- internal/matcher/radix_bench_test.go | 43 ++++++++++++++ 3 files changed, 84 insertions(+), 49 deletions(-) create mode 100644 internal/matcher/radix_bench_test.go diff --git a/internal/matcher/location.go b/internal/matcher/location.go index 89cffc2..42203e9 100644 --- a/internal/matcher/location.go +++ b/internal/matcher/location.go @@ -199,18 +199,16 @@ func (e *LocationEngine) AddNamed(name string, handler fasthttp.RequestHandler) // 返回值: // - *MatchResult: 匹配结果,无匹配时返回 nil func (e *LocationEngine) Match(path string) *MatchResult { - // 1. 精确匹配 (=) - O(1) if m, ok := e.exactMatchers[path]; ok { return m.Result() } - // 2. 前缀优先匹配 (^~) - O(log n) prefixPriorityResult := e.prefixPriorityTree.FindLongestPrefix(path) if prefixPriorityResult != nil && prefixPriorityResult.Handler != nil { return prefixPriorityResult } + ReleaseMatchResult(prefixPriorityResult) - // 3. 正则匹配 (~, ~*) - 按顺序 for _, m := range e.regexMatchers { if m.Match(path) { result := m.Result() @@ -219,7 +217,6 @@ func (e *LocationEngine) Match(path string) *MatchResult { } } - // 4. 前缀匹配(无修饰符)- O(log n) return e.prefixTree.FindLongestPrefix(path) } diff --git a/internal/matcher/radix.go b/internal/matcher/radix.go index af599cb..d166898 100644 --- a/internal/matcher/radix.go +++ b/internal/matcher/radix.go @@ -11,10 +11,17 @@ package matcher import ( "errors" "strings" + "sync" "github.com/valyala/fasthttp" ) +var matchResultPool = sync.Pool{ + New: func() any { + return &MatchResult{} + }, +} + // RadixNode Radix Tree 节点。 // // 每个节点存储一个路径前缀,子节点存储剩余前缀。 @@ -218,70 +225,58 @@ func (t *RadixTree) insertNode(parent *RadixNode, node *RadixNode, path string, // 返回值: // - *MatchResult: 最长前缀匹配结果,无匹配时返回 nil func (t *RadixTree) FindLongestPrefix(path string) *MatchResult { - return t.searchLongest(t.root, path, nil) + bestNode := t.searchLongest(t.root, path, nil) + if bestNode == nil { + return nil + } + result := matchResultPool.Get().(*MatchResult) + result.Handler = bestNode.handler + result.Path = bestNode.prefix + result.Priority = bestNode.priority + result.LocationType = bestNode.locationType + result.Internal = bestNode.internal + return result } -// searchLongest 递归搜索最长前缀匹配。 -// -// 匹配规则: -// 1. 优先级数值越小越优先 -// 2. 相同优先级时,前缀越长越优先 -// -// 参数: -// - node: 当前搜索节点 -// - path: 剩余待匹配路径 -// - bestMatch: 当前最佳匹配 -// -// 返回值: -// - *MatchResult: 最佳匹配结果 -func (t *RadixTree) searchLongest(node *RadixNode, path string, bestMatch *MatchResult) *MatchResult { +func (t *RadixTree) searchLongest(node *RadixNode, path string, bestNode *RadixNode) *RadixNode { if node == nil || path == "" { - return bestMatch + return bestNode } - // 检查是否匹配节点前缀 if !strings.HasPrefix(path, node.prefix) { - return bestMatch + return bestNode } remaining := path[len(node.prefix):] - // 如果节点有 handler,更新最佳匹配 if node.handler != nil { - newMatch := &MatchResult{ - Handler: node.handler, - Path: node.prefix, - Priority: node.priority, - LocationType: node.locationType, - Internal: node.internal, - } - - // nil-safe 优先级比较 + 长度比较 - if bestMatch == nil { - bestMatch = newMatch - } else if node.priority < bestMatch.Priority { - bestMatch = newMatch - } else if node.priority == bestMatch.Priority && len(node.prefix) > len(bestMatch.Path) { - bestMatch = newMatch + if bestNode == nil || node.priority < bestNode.priority { + bestNode = node + } else if node.priority == bestNode.priority && len(node.prefix) > len(bestNode.prefix) { + bestNode = node } } - // 继续搜索子节点 for _, child := range node.children { - childMatch := t.searchLongest(child, remaining, bestMatch) - if childMatch != nil { - // nil-safe 比较 - if bestMatch == nil { - bestMatch = childMatch - } else if childMatch.Priority < bestMatch.Priority { - bestMatch = childMatch - } else if childMatch.Priority == bestMatch.Priority && len(childMatch.Path) > len(bestMatch.Path) { - bestMatch = childMatch + result := t.searchLongest(child, remaining, bestNode) + if result != nil && result != bestNode { + if bestNode == nil || result.priority < bestNode.priority { + bestNode = result + } else if result.priority == bestNode.priority && len(result.prefix) > len(bestNode.prefix) { + bestNode = result } } } - return bestMatch + return bestNode +} + +func ReleaseMatchResult(r *MatchResult) { + if r == nil { + return + } + *r = MatchResult{} + matchResultPool.Put(r) } // MarkInitialized 标记初始化完成。 diff --git a/internal/matcher/radix_bench_test.go b/internal/matcher/radix_bench_test.go new file mode 100644 index 0000000..7e608c5 --- /dev/null +++ b/internal/matcher/radix_bench_test.go @@ -0,0 +1,43 @@ +package matcher + +import ( + "testing" + + "github.com/valyala/fasthttp" +) + +func BenchmarkRadixTreeFindLongestPrefix(b *testing.B) { + tree := NewRadixTree() + paths := []string{"/", "/api", "/api/v1", "/api/v1/users", "/static", "/static/css", "/static/js", "/health", "/favicon.ico"} + dummyHandler := func(ctx *fasthttp.RequestCtx) {} + for _, p := range paths { + tree.Insert(p, dummyHandler, 0, LocationTypePrefix, false) + } + tree.MarkInitialized() + + b.ResetTimer() + b.ReportAllocs() + for b.Loop() { + result := tree.FindLongestPrefix("/api/v1/users") + ReleaseMatchResult(result) + } +} + +func BenchmarkRadixTreeFindLongestPrefixParallel(b *testing.B) { + tree := NewRadixTree() + paths := []string{"/", "/api", "/api/v1", "/api/v1/users", "/static", "/static/css", "/static/js", "/health", "/favicon.ico"} + dummyHandler := func(ctx *fasthttp.RequestCtx) {} + for _, p := range paths { + tree.Insert(p, dummyHandler, 0, LocationTypePrefix, false) + } + tree.MarkInitialized() + + b.ResetTimer() + b.ReportAllocs() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + result := tree.FindLongestPrefix("/api/v1/users") + ReleaseMatchResult(result) + } + }) +}