Repository navigation
perf: faster route lookup (+18.7% geomean, every benchmark scenario faster) - #447
Merged
Merged
Conversation
Two optimizations to the lookup hot path, measured at +25.8% geomean across the benchmark suite (16/18 scenarios faster, up to +73% on multi-segment routes; the remaining two are at parity): * URL sanitizing: paths of 12+ chars locate '%' and the query delimiter with native String#indexOf instead of a char-by-char JS loop, and return early when the path portion contains no percent-encoding. Shorter paths keep a char scan that is now small enough for V8 to inline into find(), with the percent-decoding slow path split into a separate function that only runs when a '%' is present. * Static child lookup: static children are stored in parallel arrays keyed by the charCode of the child's first char, replacing the dictionary lookup by single-char string with a linear scan over integers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LKNnSRg1hTJsyUZaJYx3QF
The charScan/percentScan split left safeDecodeURICharScan's optimized code (with safeDecodeURIPercentScan and decodeComponentChar inlined) just over V8's inlining size limit, so find() could no longer inline the sanitizer on percent-encoded paths and paid a call plus a result allocation per lookup. Shrink the bytecode that counts against the inlining budget: * decodeComponentChar's if-chain becomes a 768-byte lookup table, with an integer-returning decodeComponentCharCode used on the scan path * the rare %25 re-encoding string surgery moves to its own function With this the whole scan chain inlines into find() again and the encoded lookup benchmarks are back at parity with main by instruction count. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LKNnSRg1hTJsyUZaJYx3QF
The trees object holds one key per HTTP method, and 35 keys is beyond V8's fast-properties limit, so it is a dictionary-mode object and every trees[method] access in find() goes through a generic IC call. Keep a reference to the GET tree in a dedicated _treeGET instance field: Router instances stay in fast mode, so for GET requests - the vast majority of real traffic - the tree load compiles to a pointer compare plus a fixed-offset field load. Other methods use the trees object as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LKNnSRg1hTJsyUZaJYx3QF
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Three optimizations to the lookup hot path, no API changes:
URL sanitizing (
lib/url-sanitizer.js) —safeDecodeURIscanned every URL char-by-char in JS on every lookup. Paths of 12+ chars now locate%and the query delimiters (?,#, and;whenuseSemicolonDelimiteris enabled) with nativeString#indexOfand return early when the path portion contains no percent-encoding, which is the common case. Shorter paths keep a char scan, split into a small function V8 reliably inlines intofind()plus a percent-decoding slow path that only runs when a%is present. To keep the percent path itself inlineable (V8 counts a callee's already-inlined bytecode against its inlining limit),decodeComponentChar's if-chain became a 768-byte lookup table with an integer-returning variant used on the scan path, and the rare%25re-encoding moved to its own function.Static child lookup (
lib/node.js) — static children were stored in an object keyed by the child's first character, so every tree level paid a dictionary lookup by single-char string (KeyedLoadICwas ~10% of profile ticks). They are now stored in parallel arrays keyed by charCode with a linear scan over integers.GET tree fast path (
index.js) —treesholds one key per HTTP method, and 35 keys is beyond V8's fast-properties limit, so it is a dictionary-mode object and everytrees[method]access goes through a generic IC call (~11% of cycles on short static lookups). A dedicated_treeGETinstance field turns the tree load for GET requests — the vast majority of real traffic — into a pointer compare plus a fixed-offset field load. Other methods usetreesas before.Benchmarks
benchmark/bench.jsonmainvs this branch (Node v24.18.0, Linux x64). To control for CPU frequency drift between runs, both suites were run twice interleaved (main, PR, main, PR) taking the best per scenario. Every scenario is faster, and the short routes that dominate real traffic gain 3-13%:Geomean: +18.7%. As a noise-free cross-check,
perf stat -e instructionson fixed 10M-lookup workloads measures -19% to -33% instructions for the long-route scenarios and parity elsewhere, andperf recordshows the IC builtins (KeyedLoadIC,LoadIC) gone from the hot profile.Testing
npm testpasses: standard, 521 tests via borp, tsd.perf stat), cycle profiles (perf record), and V8--trace-turbo-inliningused to separate real effects from timing noise and to verify inlining behavior.🤖 Generated with Claude Code
https://claude.ai/code/session_01LKNnSRg1hTJsyUZaJYx3QF