perf(matcher): eliminate heap allocations in RadixTree search with sync.Pool

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).
This commit is contained in:
xfy 2026-06-04 00:12:52 +08:00
parent d03c180f62
commit ba8c746a2e
3 changed files with 84 additions and 49 deletions

View File

@ -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)
}

View File

@ -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 标记初始化完成。

View File

@ -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)
}
})
}