Repository navigation
Conversation
…patch
llparse has emitted SSE4.2 span scanners (`_mm_cmpestri`) for the
header-value and header-field states since 2019, but they are guarded by
`#ifdef __SSE4_2__`. Node.js and the common Linux distributions build with
no `-march`, so every shipped x86-64 binary contains zero `pcmpestri` and
the fast path is dead; it is live only where the build baseline is
>= x86-64-v2 (e.g. RHEL). Passing `-msse4.2` globally was rejected before,
and an earlier per-call dispatch attempt measured slower.
This compiles the generated parser a second time with `-msse4.2` under
renamed internal symbols and lets `llhttp_execute()` select it once per
process with `__builtin_cpu_supports("sse4.2")`:
- bin/generate.ts additionally emits `llhttp_sse42_rename.h` (renames the
defined `llhttp__internal*` symbols, derived from the generated source so
it stays correct across grammar changes) and the small `llhttp_sse42.c`
wrapper that includes the generated parser under those renames. The
imported callbacks (`llhttp__on_*`) keep their names and stay shared.
- src/native/api.c dispatches in `llhttp_execute` on x86-64 GCC/clang builds
that were compiled without `-msse4.2`. Both instantiations come from the
same source, so the parser state layout is identical.
- Makefile and CMakeLists build the second object with `-msse4.2`, on
x86-64 only, skipped on MSVC.
One well-predicted branch per `llhttp_execute` call (per TCP chunk), not
per byte or per span.
Measurements (Zen 5, this parser; standalone harness, 8 interleaved runs, 95% CI):
4-header GET -4.8% [-6.5, -3.0]
32-header GET -11.6% [-12.6, -10.5]
browser GET -18.0% [-19.0, -17.1] (instructions -43%)
10 KiB POST -8.2% [-10.5, -5.9]
Node benchmark/http/bench-parser.js (compare.js, 30 runs, Welch): +4.8% (32
headers, p=3e-8), +3.9% (16), +2.3% (8), +2.2% (4); all significant.
Correctness: a libFuzzer differential harness (ASan+UBSan) drove the base
and SSE4.2 instantiations with identical random chunking and lenient-flag
combinations, comparing error code and the full callback sequence; ~5.5e8
executions, 0 divergences. The 16-byte SSE loads stay in bounds (the
generated code guards `endp - p >= 16`).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0139J1kq4EkxBPrZELPKPfF3
This branch has not been deployed
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.
Problem
llparse has emitted SSE4.2 span scanners (
_mm_cmpestri) for theheader_value,header_value_relaxedandheader_field_generalstates since #24 (2019), but they are guarded by#ifdef __SSE4_2__. Node.js builds with-O3and no x64-march, and so do the Debian/Ubuntu/Fedora/Alpine packages — so every shipped x86-64 binary contains zeropcmpestriand the scanners are dead code. They are live only where the build baseline is already ≥ x86-64-v2 (e.g. RHEL rebuilds) and on arm64 (NEON).Passing
-msse4.2globally was rejected for good reasons (#58), and the 2019 runtime-dispatch attempt (#26) checked CPU features inside the state machine on every scan and measured slower.Change
Compile the generated parser a second time with
-msse4.2under renamed internal symbols, and letllhttp_execute()pick it once per process with__builtin_cpu_supports("sse4.2"):bin/generate.tsadditionally emitsbuild/c/llhttp_sse42_rename.h—#defines that rename the definedllhttp__internal*symbols to*_sse42, derived from the generated source so it stays correct across grammar changes — and the 7-linebuild/c/llhttp_sse42.cwrapper that includes the parser under those renames. The imported callbacks (llhttp__on_*,llhttp__before_*,llhttp__after_*) keep their names and stay shared.src/native/api.cdispatches inllhttp_executeon x86-64 GCC/clang builds compiled without-msse4.2. Both instantiations come from the same generated source, so thellhttp_tlayout is identical and a parser may be driven by either.-msse4.2, on x86-64 only, skipped on MSVC (no__builtin_cpu_supports).-march ≥ x86-64-v2builds are unchanged (the dispatch compiles out; the main path already has the scanners).It is one well-predicted branch per
llhttp_executecall (per TCP chunk), not per byte or per span — the objection raised against #26 and in #58.Measurements (AMD Zen 5)
Standalone harness against this parser (8 interleaved runs, 95% CI of the time delta):
perf stat(browser): instructions −43%, branches −47%, branch-misses −62%. Requests with short header values (the 4-header case) benefit least, as expected (the 16-byte block rarely fires).Node
benchmark/http/bench-parser.jsviabenchmark/compare.js --runs 30(Welch t-test), anodebuilt from this parser vs. the stock one:Binary size: the second parser copy adds ≈ 30 KB of
.text; only one copy is hot in any process.Correctness
A libFuzzer differential harness (ASan + UBSan) drove the baseline and the SSE4.2 instantiation with identical random chunking and lenient-flag combinations, comparing the error code and the full callback sequence (kind, offset, length, state). ≈ 5.5e8 executions, 0 divergences, no sanitizer report. The 16-byte SSE loads stay in bounds — the generated code already guards
endp - p >= 16. Built and verified standalone (generator +make): base object 0pcmpestri, SSE object 3;libllhttp.aexports bothllhttp__internal_executeandllhttp__internal_execute_sse42; a program linked against it parses correctly with the SSE path selected.Fuzzing also surfaced a pre-existing infinite loop (unrelated to this change): with
lenient_header_value_relaxedset, malformed header values can makellhttp__internal__runspin — identically in the scalar and SSE builds. It looks fixed onmain(#882, unreleased in 9.4.3); happy to file a separate issue with the reproducer.Notes
Companion one-liner for Node:
deps/llhttp/llhttp.gypneeds the same second target with-msse4.2once a release includes this — I can open that PR against nodejs/node after a release. If you'd prefer llparse to emit thellhttp__internal__c_*helpers asstatic, the rename header could shrink to just the two entry points; glad to take whichever direction you prefer.🤖 Generated with Claude Code
https://claude.ai/code/session_0139J1kq4EkxBPrZELPKPfF3