Skip to content

perf(router): cache-friendly static child lookup#3024

Open
vishr wants to merge 1 commit into
masterfrom
perf/router-static-child-lookup
Open

perf(router): cache-friendly static child lookup#3024
vishr wants to merge 1 commit into
masterfrom
perf/router-static-child-lookup

Conversation

@vishr

@vishr vishr commented Jun 19, 2026

Copy link
Copy Markdown
Member

Split out of #3023 at @aldas's request so the router and JSON changes land as independent PRs.

What

findStaticChild scanned []*node and dereferenced every child just to read its one-byte label — a pointer chase per static step on the routing hot path. This adds a parallel scLabels []byte (kept in sync in addStaticChild, newNode, the insert split, and Remove) and scans that contiguous slice instead, indexing into staticChildren only on a hit.

It also folds the is-leaf recomputation — previously duplicated across 5 sites, with the Remove copy subtly using len()==0 while the others used == nil — into a single refreshLeaf() helper that uses len(), so it is correct whether staticChildren is nil or an emptied-but-non-nil slice left after a removal.

Numbers

benchstat (n=6):
RouterStaticRoutes-14   8.49µs -> 8.01µs   -5.67%  (p=0.002)
RouterGitHubAPI-14      16.93µs -> 16.45µs  -2.86%  (p=0.002)
RouterParseAPI / GooglePlusAPI: neutral (param-heavy)
allocations: 0, unchanged

The companion ServeHTTP benchmark harness lands with #3023 (the JSON PR); routing benchmarks there exercise this change once both merge.

🤖 Generated with Claude Code

findStaticChild scanned []*node and dereferenced every child to read its
one-byte label — a pointer chase per static step. Maintain a parallel
scLabels []byte (kept in sync in addStaticChild, newNode, the insert
split, and Remove) and scan that contiguous slice instead, indexing into
staticChildren only on a hit.

  benchstat (n=6):
  RouterStaticRoutes-14   8.49µ -> 8.01µ   -5.67%  (p=0.002)
  RouterGitHubAPI-14      16.93µ -> 16.45µ  -2.86%  (p=0.002)
  RouterParseAPI / GooglePlusAPI: neutral (param-heavy)
  allocations: 0, unchanged

Also fold the is-leaf recomputation (duplicated across 5 sites, with the
Remove copy subtly using len()==0 vs == nil) into a single refreshLeaf()
helper that uses len() so it is correct whether staticChildren is nil or
an emptied-but-non-nil slice left after a removal.

Split out of #3023 so the router and JSON changes can be reviewed
independently (requested by @aldas).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@aldas aldas left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@vishr , If you want to do experiments - in v5 the router has interface and we could try to make another router implementation that stores routes in Method based trees so more uncommon method routes could be accessed with less instructions.

and maybe create optional interface for routers that will be called inside StartConfig.start to recompute routers just before http server is started so router could recompute it internal representation (ala which to router where each method has their own tree, or another idea for future - router that do not support wildcard/param routes if the initialized routes have none - therefore being more efficient.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants