Skip to content

perf(router): return the GET handler before the method switch - #3159

Closed
vishr wants to merge 1 commit into
masterfrom
perf/v5-router
Closed

vishr wants to merge 1 commit into
masterfrom
perf/v5-router

Conversation

@vishr

@vishr vishr commented Oct 5, 2026 •

Copy link
Copy Markdown
Member

GET is by far the most common request method. routeMethods.find resolved it through the method switch (which Go 1.27 compiles to a jump table on string length followed by byte comparisons). This returns the GET handler first when one is registered.

Behavior is unchanged:

  • GET with a handler returns it, as before.
  • GET without one (Any-only node, or a 404/405 search) still goes through the switch and the Any fallback.
  • An independent check compared the old and new find over 5.2M combinations of node handlers, methods (including custom, RouteAny, RouteNotFound), fallbackToAny and autoHandleHEAD, and found 0 differences. An end-to-end dump of status, Allow header, RouteInfo and path values across 44 paths × 18 methods was byte-identical to master.

Benchmarks:

  • The honest gain is about 2% on router benchmarks, measured against a placebo build (Apple M3 Max, -cpu=1, 17 interleaved samples).
  • The placebo adds the same check with a condition that never matches, so its code size and layout match this PR. Compared with master, the placebo is already -2.3% faster from layout alone. This PR is -4.3% against master and -2.1% against the placebo.
  • Raw comparisons against master, including CI's x86 run (router -4% to -12%), therefore overstate the effect. Gains on the *Misses benchmarks are layout only: 404 lookups never call routeMethods.find.
  • Non-GET requests pay one extra comparison, about +0.3 ns per find call in an isolated micro-benchmark. No router-level benchmark shows it, including static routes with only POST/PUT/HEAD and GitHub's mixed-method routes.
  • Allocations are unchanged (0).

go test -race ./..., go vet, staticcheck and golint pass.

Other options tried and rejected, measured against master without a placebo:

  • Comparing a node's whole prefix in one string comparison instead of the byte-by-byte LCP loop: hits got faster, but misses got 4–13% slower in all three variants.
  • bytes.IndexByte for static child lookup: slower than the short label scan.
  • Splitting the method switch into a separate non-GET function: smaller gain, and ParamsAndAny got 2% slower.

GET is by far the most common request method. routeMethods.find matched it
through a string switch over every method. Return the GET handler first
when one is registered; other methods, and GET with only an Any handler,
still go through the switch unchanged.
@vishr
vishr requested a balanced review from Copilot October 6, 2026 02:53

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🟢 Approval recommended

The focused optimization preserves existing routing semantics and introduces no unresolved issues.

Review effort: Balanced
Findings: None

What changed in this PR

Optimizes common GET route lookup without changing fallback behavior.

Changes:

  • Returns registered GET handlers before the method switch.
  • Preserves existing Any and missing-handler fallbacks.
File Description
router.go Adds the GET fast path in routeMethods.find.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@vishr

vishr commented Oct 6, 2026

Copy link
Copy Markdown
Member Author

Closing. Measured against a placebo build with the same code layout, the GET early return gains only about 2% on router benchmarks (about 0.5 ns per GET request). That is below the bar we set for performance changes, and it adds a special case to the method lookup. The raw -3.7% to -3.9% figures in the description were mostly code-layout effects, as explained there.

Other router ideas measured and rejected in the same exercise: comparing whole node prefixes at once (misses got 4–13% slower), and bytes.IndexByte for static child lookup (slower). The router is already about 28–45 ns per request with 0 allocations; the remaining cost is the radix-tree walk itself.

@vishr vishr closed this Oct 6, 2026
@vishr
vishr deleted the perf/v5-router branch October 6, 2026 03:22
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