From 0bb1d6a81627dbbe88f1504e3955deb7015e134d Mon Sep 17 00:00:00 2001 From: Jens Sonnenberg <10518877+jenswl@users.noreply.github.com> Date: Sun, 8 Nov 2026 17:38:38 +0100 Subject: [PATCH] Run the SSE4.2 span scanners on default x86-64 builds via runtime dispatch 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 Claude-Session: https://claude.ai/code/session_0139J1kq4EkxBPrZELPKPfF3 --- CMakeLists.txt | 12 ++++++++++++ Makefile | 20 +++++++++++++++++--- bin/generate.ts | 31 +++++++++++++++++++++++++++++++ src/native/api.c | 24 ++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 3 deletions(-) mode change 100755 => 100644 bin/generate.ts diff --git a/CMakeLists.txt b/CMakeLists.txt index 54ae1b1a..500b6a89 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,6 +35,18 @@ set(LLHTTP_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/api.c ) +# On x86-64, compile a second instantiation of the parser with SSE4.2 enabled so +# llhttp_execute() can select it at runtime (see src/api.c). The default build +# passes no -march, so the SSE4.2 span scanners in the generated code are +# otherwise never compiled in. Skipped on MSVC (no __builtin_cpu_supports). +if(NOT MSVC AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|amd64|AMD64)$") + list(APPEND LLHTTP_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/llhttp_sse42.c) + set_source_files_properties( + ${CMAKE_CURRENT_SOURCE_DIR}/src/llhttp_sse42.c + PROPERTIES COMPILE_OPTIONS "-msse4.2" + ) +endif() + set(LLHTTP_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/include/llhttp.h ) diff --git a/Makefile b/Makefile index 16fe819f..f92fbe65 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,15 @@ endif INCLUDES += -Ibuild/ +# On x86-64, also build a second instantiation of the generated parser with +# -msse4.2 so llhttp_execute() can select the SSE4.2 span scanners at runtime +# (see src/native/api.c). The default build passes no -march, so without this +# the SSE4.2 code generated under `#ifdef __SSE4_2__` is never compiled in. +ARCH ?= $(shell uname -m) +ifneq (,$(filter $(ARCH),x86_64 amd64 AMD64)) + SSE42_OBJ = build/c/llhttp_sse42.o +endif + INSTALL ?= install PREFIX ?= /usr/local LIBDIR = $(PREFIX)/lib @@ -23,17 +32,20 @@ clean: rm -rf build/ rm -rf test/tmp -build/libllhttp.so: build/c/llhttp.o build/native/api.o \ +build/libllhttp.so: build/c/llhttp.o $(SSE42_OBJ) build/native/api.o \ build/native/http.o $(CLANG) -shared $^ -o $@ -build/libllhttp.a: build/c/llhttp.o build/native/api.o \ +build/libllhttp.a: build/c/llhttp.o $(SSE42_OBJ) build/native/api.o \ build/native/http.o - $(AR) rcs $@ build/c/llhttp.o build/native/api.o build/native/http.o + $(AR) rcs $@ build/c/llhttp.o $(SSE42_OBJ) build/native/api.o build/native/http.o build/c/llhttp.o: build/c/llhttp.c $(CLANG) $(CFLAGS) $(INCLUDES) -c $< -o $@ +build/c/llhttp_sse42.o: build/c/llhttp_sse42.c build/c/llhttp.c + $(CLANG) $(CFLAGS) -msse4.2 $(INCLUDES) -c $< -o $@ + build/native/%.o: src/native/%.c build/llhttp.h src/native/api.h \ build/native $(CLANG) $(CFLAGS) $(INCLUDES) -c $< -o $@ @@ -52,6 +64,8 @@ release: clean all generate mkdir -p release/include cp -rf build/llhttp.h release/include/ cp -rf build/c/llhttp.c release/src/ + cp -rf build/c/llhttp_sse42.c release/src/ + cp -rf build/c/llhttp_sse42_rename.h release/src/ cp -rf src/native/*.c release/src/ cp -rf src/llhttp.gyp release/ cp -rf src/common.gypi release/ diff --git a/bin/generate.ts b/bin/generate.ts old mode 100755 new mode 100644 index 727269dd..dbb4c993 --- a/bin/generate.ts +++ b/bin/generate.ts @@ -11,6 +11,8 @@ const semverRE = /^(?0|[1-9]\d*)\.(?0|[1-9]\d*)\.(?0|[1-9]\ const C_FILE = resolve(__dirname, '../build/c/llhttp.c'); const HEADER_FILE = resolve(__dirname, '../build/llhttp.h'); +const SSE42_RENAME_FILE = resolve(__dirname, '../build/c/llhttp_sse42_rename.h'); +const SSE42_C_FILE = resolve(__dirname, '../build/c/llhttp_sse42.c'); const pkg = JSON.parse( readFileSync(resolve(__dirname, '..', 'package.json')).toString(), @@ -49,3 +51,32 @@ ${nativeHeaders} mkdirSync(dirname(C_FILE), { recursive: true }); writeFileSync(HEADER_FILE, headers); writeFileSync(C_FILE, generated.c); + +// A second instantiation of the generated parser, compiled with -msse4.2, lets +// llhttp_execute() use the SSE4.2 span scanners at runtime on default x86-64 +// builds (which pass no -march, so the `#ifdef __SSE4_2__` code is otherwise +// dead). The parser defines symbols prefixed `llhttp__internal`; the callbacks +// it imports are prefixed `llhttp__on`/`llhttp__before`/`llhttp__after` and stay +// shared. Rename only the defined `llhttp__internal*` symbols so the second copy +// links beside the first. Derived from the generated source, so it stays correct +// across grammar changes. +const internalSymbols = Array.from( + new Set(generated.c.match(/\bllhttp__internal[0-9A-Za-z_]*/g) ?? []), +).sort(); +const renameHeader = `/* Generated by bin/generate.ts. Renames the defined symbols of llhttp.c so a + * second, -msse4.2 instantiation (llhttp_sse42.c) can link beside the first. */ +#ifndef INCLUDE_LLHTTP_SSE42_RENAME_H_ +#define INCLUDE_LLHTTP_SSE42_RENAME_H_ +${internalSymbols.map((s) => `#define ${s} ${s}_sse42`).join('\n')} +#endif /* INCLUDE_LLHTTP_SSE42_RENAME_H_ */ +`; +const sse42Wrapper = `/* Generated by bin/generate.ts. See src/native/api.c: a second instantiation of + * the generated parser with the SSE4.2 span scanners enabled, selected once at + * runtime by llhttp_execute(). Built with -msse4.2 by the build system. */ +#if defined(__x86_64__) || defined(_M_X64) +#include "llhttp_sse42_rename.h" +#include "llhttp.c" +#endif +`; +writeFileSync(SSE42_RENAME_FILE, renameHeader); +writeFileSync(SSE42_C_FILE, sse42Wrapper); diff --git a/src/native/api.c b/src/native/api.c index ae5e862d..3c483659 100644 --- a/src/native/api.c +++ b/src/native/api.c @@ -122,9 +122,33 @@ void llhttp_reset(llhttp_t* parser) { } +#if (defined(__x86_64__) || defined(_M_X64)) && !defined(__SSE4_2__) && \ + (defined(__GNUC__) || defined(__clang__)) +/* A second instantiation of the generated parser, compiled with -msse4.2 (see + * llhttp_sse42.c), which turns on the SSE4.2 span scanners that the generated + * code guards with `#ifdef __SSE4_2__`. The default x86-64 build (no -march) + * never contains them, so pick the SSE4.2 build once at runtime. Both + * instantiations come from the same generated source, so the parser state + * layout is identical and a parser may be driven by either. */ +llhttp_errno_t llhttp__internal_execute_sse42(llhttp_t* state, const char* p, + const char* endp); + +static int llhttp__sse42 = -1; + +llhttp_errno_t llhttp_execute(llhttp_t* parser, const char* data, size_t len) { + if (llhttp__sse42 < 0) { + llhttp__sse42 = __builtin_cpu_supports("sse4.2") ? 1 : 0; + } + if (llhttp__sse42) { + return llhttp__internal_execute_sse42(parser, data, data + len); + } + return llhttp__internal_execute(parser, data, data + len); +} +#else llhttp_errno_t llhttp_execute(llhttp_t* parser, const char* data, size_t len) { return llhttp__internal_execute(parser, data, data + len); } +#endif void llhttp_settings_init(llhttp_settings_t* settings) {