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:
parent
d03c180f62
commit
ba8c746a2e
@ -199,18 +199,16 @@ func (e *LocationEngine) AddNamed(name string, handler fasthttp.RequestHandler)
|
|||||||
// 返回值:
|
// 返回值:
|
||||||
// - *MatchResult: 匹配结果,无匹配时返回 nil
|
// - *MatchResult: 匹配结果,无匹配时返回 nil
|
||||||
func (e *LocationEngine) Match(path string) *MatchResult {
|
func (e *LocationEngine) Match(path string) *MatchResult {
|
||||||
// 1. 精确匹配 (=) - O(1)
|
|
||||||
if m, ok := e.exactMatchers[path]; ok {
|
if m, ok := e.exactMatchers[path]; ok {
|
||||||
return m.Result()
|
return m.Result()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 前缀优先匹配 (^~) - O(log n)
|
|
||||||
prefixPriorityResult := e.prefixPriorityTree.FindLongestPrefix(path)
|
prefixPriorityResult := e.prefixPriorityTree.FindLongestPrefix(path)
|
||||||
if prefixPriorityResult != nil && prefixPriorityResult.Handler != nil {
|
if prefixPriorityResult != nil && prefixPriorityResult.Handler != nil {
|
||||||
return prefixPriorityResult
|
return prefixPriorityResult
|
||||||
}
|
}
|
||||||
|
ReleaseMatchResult(prefixPriorityResult)
|
||||||
|
|
||||||
// 3. 正则匹配 (~, ~*) - 按顺序
|
|
||||||
for _, m := range e.regexMatchers {
|
for _, m := range e.regexMatchers {
|
||||||
if m.Match(path) {
|
if m.Match(path) {
|
||||||
result := m.Result()
|
result := m.Result()
|
||||||
@ -219,7 +217,6 @@ func (e *LocationEngine) Match(path string) *MatchResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 前缀匹配(无修饰符)- O(log n)
|
|
||||||
return e.prefixTree.FindLongestPrefix(path)
|
return e.prefixTree.FindLongestPrefix(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -11,10 +11,17 @@ package matcher
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/valyala/fasthttp"
|
"github.com/valyala/fasthttp"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var matchResultPool = sync.Pool{
|
||||||
|
New: func() any {
|
||||||
|
return &MatchResult{}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
// RadixNode Radix Tree 节点。
|
// RadixNode Radix Tree 节点。
|
||||||
//
|
//
|
||||||
// 每个节点存储一个路径前缀,子节点存储剩余前缀。
|
// 每个节点存储一个路径前缀,子节点存储剩余前缀。
|
||||||
@ -218,70 +225,58 @@ func (t *RadixTree) insertNode(parent *RadixNode, node *RadixNode, path string,
|
|||||||
// 返回值:
|
// 返回值:
|
||||||
// - *MatchResult: 最长前缀匹配结果,无匹配时返回 nil
|
// - *MatchResult: 最长前缀匹配结果,无匹配时返回 nil
|
||||||
func (t *RadixTree) FindLongestPrefix(path string) *MatchResult {
|
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 递归搜索最长前缀匹配。
|
func (t *RadixTree) searchLongest(node *RadixNode, path string, bestNode *RadixNode) *RadixNode {
|
||||||
//
|
|
||||||
// 匹配规则:
|
|
||||||
// 1. 优先级数值越小越优先
|
|
||||||
// 2. 相同优先级时,前缀越长越优先
|
|
||||||
//
|
|
||||||
// 参数:
|
|
||||||
// - node: 当前搜索节点
|
|
||||||
// - path: 剩余待匹配路径
|
|
||||||
// - bestMatch: 当前最佳匹配
|
|
||||||
//
|
|
||||||
// 返回值:
|
|
||||||
// - *MatchResult: 最佳匹配结果
|
|
||||||
func (t *RadixTree) searchLongest(node *RadixNode, path string, bestMatch *MatchResult) *MatchResult {
|
|
||||||
if node == nil || path == "" {
|
if node == nil || path == "" {
|
||||||
return bestMatch
|
return bestNode
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否匹配节点前缀
|
|
||||||
if !strings.HasPrefix(path, node.prefix) {
|
if !strings.HasPrefix(path, node.prefix) {
|
||||||
return bestMatch
|
return bestNode
|
||||||
}
|
}
|
||||||
|
|
||||||
remaining := path[len(node.prefix):]
|
remaining := path[len(node.prefix):]
|
||||||
|
|
||||||
// 如果节点有 handler,更新最佳匹配
|
|
||||||
if node.handler != nil {
|
if node.handler != nil {
|
||||||
newMatch := &MatchResult{
|
if bestNode == nil || node.priority < bestNode.priority {
|
||||||
Handler: node.handler,
|
bestNode = node
|
||||||
Path: node.prefix,
|
} else if node.priority == bestNode.priority && len(node.prefix) > len(bestNode.prefix) {
|
||||||
Priority: node.priority,
|
bestNode = node
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 继续搜索子节点
|
|
||||||
for _, child := range node.children {
|
for _, child := range node.children {
|
||||||
childMatch := t.searchLongest(child, remaining, bestMatch)
|
result := t.searchLongest(child, remaining, bestNode)
|
||||||
if childMatch != nil {
|
if result != nil && result != bestNode {
|
||||||
// nil-safe 比较
|
if bestNode == nil || result.priority < bestNode.priority {
|
||||||
if bestMatch == nil {
|
bestNode = result
|
||||||
bestMatch = childMatch
|
} else if result.priority == bestNode.priority && len(result.prefix) > len(bestNode.prefix) {
|
||||||
} else if childMatch.Priority < bestMatch.Priority {
|
bestNode = result
|
||||||
bestMatch = childMatch
|
|
||||||
} else if childMatch.Priority == bestMatch.Priority && len(childMatch.Path) > len(bestMatch.Path) {
|
|
||||||
bestMatch = childMatch
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return bestMatch
|
return bestNode
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReleaseMatchResult(r *MatchResult) {
|
||||||
|
if r == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
*r = MatchResult{}
|
||||||
|
matchResultPool.Put(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarkInitialized 标记初始化完成。
|
// MarkInitialized 标记初始化完成。
|
||||||
|
|||||||
43
internal/matcher/radix_bench_test.go
Normal file
43
internal/matcher/radix_bench_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user