You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
ext/json: Add SSE2/NEON fast path for clean-string runs in php_json_escape_string() - #23675
json_encode() walks every string byte-by-byte in php_json_escape_string(), even for the common case of plain ASCII needing no escaping. This adds a vectorized (SSE2/NEON) fast path that scans 16 bytes at a time, finds every character that needs escaping in one pass, and bulk-copies the clean runs in between. No output or behavior change — purely a speed improvement, with wins across clean ASCII, HTML-heavy, and URL-heavy content and no meaningful regression anywhere measured.
Solution
On a 16-byte chunk, the fast path finds every character that needs escaping in one vector comparison and handles all of them before moving to the next chunk, rather than dropping into a slower per-byte loop as soon as it hits one. It also only compares against characters actually being escaped under the active encoder options (so plain </>/&/' don't cost anything when the matching flags aren't set), skips the vector setup for strings shorter than one chunk, and uses one shared implementation across x86 and ARM.
Benchmarks
Speedup range across small/medium/large corpus sizes, per CPU, TRIALS=7:
Corpus
Intel i5-14500
Apple M2
AMD Ryzen 5 7500F
clean_ascii
1.24× – 1.63×
1.13× – 1.22×
1.17× – 1.36×
html_heavy
2.43× – 3.60×
1.91× – 1.95×
1.63× – 2.23×
mixed
2.73× – 4.66×
1.55× – 2.21×
1.54× – 2.08×
url_heavy
1.62× – 2.10×
1.02× – 1.02×
0.99× – 1.18×
non_ascii
1.04× – 1.06×
1.04× – 1.04×
0.97× – 1.00×
(Only one cell out of 42 measured regresses (non_ascii, AMD, 0.97×) — within noise. Everything else is flat-to-positive.
Testing
Output byte-identical to master across all corpora, 15 encoder-flag combinations, and boundary lengths around the 16-byte chunk edge.
Correctness fuzzed against a reference scalar implementation (1.65M+ runs) before benchmarking.
New tests: json_encode_sse2_boundary.phpt, json_encode_sse2_options.phpt, json_encode_sse2_fuzz.phpt. Full ext/json suite passes.
Why this doesn't need an RFC
Per CONTRIBUTING.md, RFCs are for feature requests and behavior-changing proposals. This changes no API, no ini directive, and no observable output — only speed.
Alternatives
A PECL/third-party extension (simdjson_php, decode-only; a fork adds encoding) exists but isn't in core, re-implements the encoder, and can drift from php-src's exact escaping semantics.
A different wire format (igbinary, MessagePack) avoids escaping overhead but isn't a substitute when JSON is required for interop.
Related Work
af112f6 — SSE2 url_encode(), closest prior art. Similar SSE2 precedent in stripslashes(), strtr(), mb_strlen().
Optimize JSON string encoding #17734 bundles a UTF-8 rework and this kind of fast path into one large PR; this PR stays scoped to php_json_escape_string().
adapik
changed the title
json: Add SSE2/NEON fast path for clean-string runs in php_json_escape_string()
ext/json: Add SSE2/NEON fast path for clean-string runs in php_json_escape_string()
Sep 14, 2026
I measured this on current master (bd6c637) with four compilers, the same way as #23682.
Correctness. On current master it needs one change to build: ZEND_STATIC_ASSERT() was replaced by static_assert() in c5e45a1. With that, output and json_last_error() are byte-identical to master on every pair of bytes and every three-byte sequence with an E0..EF lead (six flag sets), every byte value at every position of ASCII strings 1–24 bytes long, literal keys, and 200,000 random strings under 11 flag sets, with GCC 13.3, GCC 14.2, clang 18.1 and clang 20.1.
Numbers. Cycles / instructions per json_encode() call relative to master; perf stat over N calls minus the same process with 0 calls, median of 5 alternating runs, release builds, i7-13700H pinned to one P-core:
4 KiB string unless noted
GCC 13.3
GCC 14.2
clang 18.1
clang 20.1
ASCII, 16 bytes
0.87 / 0.85
0.95 / 0.86
0.94 / 0.80
0.96 / 0.78
ASCII, 256 bytes
0.41 / 0.39
0.45 / 0.41
0.43 / 0.38
0.42 / 0.37
ASCII
0.22 / 0.22
0.25 / 0.25
0.21 / 0.21
0.22 / 0.21
HTML
0.21 / 0.36
0.23 / 0.38
0.35 / 0.33
0.34 / 0.33
URLs
0.53 / 0.74
0.57 / 0.75
0.74 / 0.67
0.76 / 0.66
a quote every 16 bytes
0.29 / 0.47
0.27 / 0.49
0.60 / 0.45
0.66 / 0.45
a newline every 40 bytes
0.24 / 0.34
0.20 / 0.36
0.41 / 0.33
0.41 / 0.33
ASCII with an é every 31 bytes on average
0.25 / 0.47
0.25 / 0.49
0.45 / 0.44
0.46 / 0.44
a quote in every byte
1.19 / 1.10
1.15 / 1.07
1.01 / 0.95
0.88 / 0.93
Cyrillic
1.13 / 1.08
1.18 / 1.07
0.91 / 0.90
0.98 / 0.90
Cyrillic, JSON_UNESCAPED_UNICODE
0.89 / 1.04
1.07 / 1.03
1.00 / 0.90
0.95 / 0.91
CJK
1.10 / 1.04
1.04 / 1.02
0.89 / 0.88
0.94 / 0.88
French
0.77 / 0.85
0.80 / 0.85
0.79 / 0.74
0.84 / 0.74
1,000 arrays of 10 fields
0.93 / 0.94
0.92 / 0.92
0.95 / 0.88
0.94 / 0.87
1,000 objects
0.91 / 0.94
0.93 / 0.92
0.98 / 0.88
0.99 / 0.87
Strings of 256 bytes and more that are mostly ASCII are 1.3–5× faster with every compiler; 16-byte strings 4–13%.
Cyrillic, CJK and a string of quotes are 4–19% slower with GCC, at 2–10% more instructions than master. With clang the same inputs run 5–12% fewer instructions and are at 0.88–1.01 in cycles.
With default flags every byte of a non-ASCII character needs escaping, so on that text the vector path skips nothing and each character also pays the walk over the chunk's bitmask (tzcnt, the compare with chunk_left, the shift, the counters).
The difference between compilers is on master's side. On the Cyrillic input, php_json_escape_string() itself (without php_next_utf8_char()) runs 74 instructions per non-ASCII character on master (all of its work on that input divided by the number of such characters) with GCC 13 and 14, and 93 with clang 18 and 20: clang stores seven registers to the stack before every call to php_next_utf8_char() and loads them back after it, GCC does not. With this PR it is 83 with GCC and 79 with clang: the bitmask walk adds about 9 per character, and with clang the new loop also no longer spills around the call (callgrind, per-instruction counts).
With #24283 on top.#24283 decodes valid two- and three-byte UTF-8 inline in this function. Merged onto this PR (the one call in php_json_escape_dirty_char() switched to php_next_utf8_char_inline()), output is identical under the same checks, and cycles / instructions relative to master are:
GCC 13.3
GCC 14.2
clang 18.1
clang 20.1
ASCII
0.22 / 0.22
0.27 / 0.25
0.21 / 0.21
0.21 / 0.21
HTML
0.20 / 0.36
0.23 / 0.38
0.35 / 0.34
0.38 / 0.34
URLs
0.55 / 0.73
0.55 / 0.75
0.72 / 0.70
0.77 / 0.69
a quote in every byte
1.18 / 1.10
1.10 / 1.07
0.92 / 0.91
0.85 / 0.88
Cyrillic
0.85 / 0.82
0.87 / 0.81
0.68 / 0.67
0.66 / 0.66
Cyrillic, JSON_UNESCAPED_UNICODE
0.67 / 0.74
0.83 / 0.74
0.80 / 0.69
0.74 / 0.69
CJK
0.84 / 0.79
0.78 / 0.77
0.58 / 0.66
0.62 / 0.64
French
0.66 / 0.69
0.60 / 0.70
0.64 / 0.62
0.69 / 0.61
ASCII with an é every 31 bytes on average
0.21 / 0.40
0.21 / 0.42
0.39 / 0.43
0.43 / 0.38
1,000 arrays of 10 fields
0.90 / 0.94
0.98 / 0.92
0.93 / 0.88
0.92 / 0.87
So together nothing non-ASCII stays slower than master; the one row left above 1.00 is a string made only of quotes with GCC (1.10–1.18). Merging the two needs only that one call changed.
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
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
json_encode()walks every string byte-by-byte inphp_json_escape_string(), even for the common case of plain ASCII needing no escaping. This adds a vectorized (SSE2/NEON) fast path that scans 16 bytes at a time, finds every character that needs escaping in one pass, and bulk-copies the clean runs in between. No output or behavior change — purely a speed improvement, with wins across clean ASCII, HTML-heavy, and URL-heavy content and no meaningful regression anywhere measured.Solution
On a 16-byte chunk, the fast path finds every character that needs escaping in one vector comparison and handles all of them before moving to the next chunk, rather than dropping into a slower per-byte loop as soon as it hits one. It also only compares against characters actually being escaped under the active encoder options (so plain
</>/&/'don't cost anything when the matching flags aren't set), skips the vector setup for strings shorter than one chunk, and uses one shared implementation across x86 and ARM.Benchmarks
Speedup range across small/medium/large corpus sizes, per CPU,
TRIALS=7:(Only one cell out of 42 measured regresses (
non_ascii, AMD, 0.97×) — within noise. Everything else is flat-to-positive.Testing
masteracross all corpora, 15 encoder-flag combinations, and boundary lengths around the 16-byte chunk edge.json_encode_sse2_boundary.phpt,json_encode_sse2_options.phpt,json_encode_sse2_fuzz.phpt. Fullext/jsonsuite passes.Why this doesn't need an RFC
Per CONTRIBUTING.md, RFCs are for feature requests and behavior-changing proposals. This changes no API, no ini directive, and no observable output — only speed.
Alternatives
simdjson_php, decode-only; a fork adds encoding) exists but isn't in core, re-implements the encoder, and can drift from php-src's exact escaping semantics.Related Work
af112f6— SSE2url_encode(), closest prior art. Similar SSE2 precedent instripslashes(),strtr(),mb_strlen().php_json_escape_string().