Skip to content

Commit 0515e52

Browse files
authored
perf(lookup): short-circuit 1-byte keys and inline route.match (#102)
1 parent ba56b3c commit 0515e52

4 files changed

Lines changed: 44 additions & 6 deletions

File tree

fox.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,9 @@ func (fox *Router) Name(name string) *Route {
361361
// (trailing slash action recommended). This function is safe for concurrent use by multiple goroutine and while
362362
// mutation on routes are ongoing. See also [Router.Lookup] as an alternative.
363363
func (fox *Router) Match(method string, r *http.Request) (route *Route, tsr bool) {
364+
if method == "" {
365+
return nil, false
366+
}
364367
tree := fox.getTree()
365368
c := tree.pool.Get().(*Context)
366369
defer tree.pool.Put(c)
@@ -381,6 +384,9 @@ func (fox *Router) Match(method string, r *http.Request) (route *Route, tsr bool
381384
// [Route] and a [Context]. The [Context] should always be closed if non-nil. This function is safe for
382385
// concurrent use by multiple goroutine and while mutation on routes are ongoing. See also [Router.Match] as an alternative.
383386
func (fox *Router) Lookup(w ResponseWriter, r *http.Request) (route *Route, cc *Context, tsr bool) {
387+
if r.Method == "" {
388+
return nil, nil, false
389+
}
384390
tree := fox.getTree()
385391
c := tree.pool.Get().(*Context)
386392
c.resetWithWriter(w, r)
@@ -459,6 +465,10 @@ func (fox *Router) NewRoute(methods []string, pattern string, handler HandlerFun
459465
rte.methods = slices.Compact(rte.methods)
460466
}
461467

468+
if len(rte.methods) == 1 && len(rte.matchers) == 0 {
469+
rte.methodFast = rte.methods[0]
470+
}
471+
462472
return rte, nil
463473
}
464474

node.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,10 @@ Walk:
106106
if idx < num && matched.statics[idx].label == label {
107107
child := matched.statics[idx]
108108
keyLen := len(child.key)
109-
if keyLen <= len(search) && stringsutil.EqualStringsASCIIIgnoreCase(search[:keyLen], child.key) {
109+
// keyLen == 1 short-circuits the case-insensitive compare: hostname keys are
110+
// stored lowercase (uppercase is rejected at parse time) and the label match
111+
// already proved lowercase(search[0]) == child.key[0].
112+
if keyLen == 1 || (keyLen <= len(search) && stringsutil.EqualStringsASCIIIgnoreCase(search[:keyLen], child.key)) {
110113
if len(matched.params) > 0 || len(matched.wildcards) > 0 {
111114
*c.skipStack = append(*c.skipStack, skipNode{
112115
node: matched,
@@ -332,8 +335,10 @@ Walk:
332335
child := matched.statics[idx]
333336
keyLen := len(child.key)
334337
// While this is less performant than byte-by-byte comparaison for reasonable search size,
335-
// direct == comparaison on string scale way better on long route.
336-
if keyLen <= len(search) && search[:keyLen] == child.key {
338+
// direct == comparaison on string scale way better on long route. The keyLen == 1 case
339+
// short-circuits the memequal call: we already verified search[0] == child.key[0] via the
340+
// label match above.
341+
if keyLen == 1 || (keyLen <= len(search) && search[:keyLen] == child.key) {
337342
if len(matched.params) > 0 || len(matched.wildcards) > 0 {
338343
*c.skipStack = append(*c.skipStack, skipNode{
339344
node: matched,

route.go

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ type Route struct {
1919
mws []middleware
2020
params []string
2121
matchers []Matcher
22+
methodFast string
2223
pattern pattern
2324
priority uint
2425
handleSlash TrailingSlashOption
@@ -135,12 +136,27 @@ func (r *Route) String() string {
135136
// match reports whether the request satisfies this route's method constraint (if any)
136137
// and all attached matchers.
137138
func (r *Route) match(method string, c RequestContext) bool {
138-
// Fast path for common cases: no methods or single method
139+
// Fast path: routes with exactly one method and no matchers cache that
140+
// method in methodFast.
141+
if r.methodFast == method {
142+
return true
143+
}
144+
return r.matchSlow(method, c)
145+
}
146+
147+
// matchSlow handles the cases match's fast path does not cover: zero or many
148+
// methods, and routes with matchers. It is kept out-of-line so match remains
149+
// inlinable.
150+
//
151+
//go:noinline
152+
func (r *Route) matchSlow(method string, c RequestContext) bool {
139153
methods := r.methods
140154
switch len(methods) {
141155
case 0:
142-
// No method constraint
156+
// No method constraint.
143157
case 1:
158+
// Avoid the slices.Contains overhead for the single-method case (which
159+
// match's fast path leaves to us when matchers are present).
144160
if methods[0] != method {
145161
return false
146162
}
@@ -149,7 +165,6 @@ func (r *Route) match(method string, c RequestContext) bool {
149165
return false
150166
}
151167
}
152-
153168
for _, m := range r.matchers {
154169
if !m.Match(c) {
155170
return false

txn.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,10 @@ func (txn *Txn) Match(method string, r *http.Request) (route *Route, tsr bool) {
285285
panic(ErrSettledTxn)
286286
}
287287

288+
if method == "" {
289+
return nil, false
290+
}
291+
288292
tree := txn.rootTxn.tree
289293
c := tree.pool.Get().(*Context)
290294
defer tree.pool.Put(c)
@@ -309,6 +313,10 @@ func (txn *Txn) Lookup(w ResponseWriter, r *http.Request) (route *Route, cc *Con
309313
panic(ErrSettledTxn)
310314
}
311315

316+
if r.Method == "" {
317+
return nil, nil, false
318+
}
319+
312320
tree := txn.rootTxn.tree
313321
c := tree.pool.Get().(*Context)
314322
c.resetWithWriter(w, r)

0 commit comments

Comments
 (0)