[php-src] PR #24666: ext/json: Speed up scanning and escaping of strings

From: Date: Sat, 24 Oct 2026 23:15:08 +0000
Subject: [php-src] PR #24666: ext/json: Speed up scanning and escaping of strings
Groups: php.git-pulls 
Request: Send a blank email to git-pulls+get-39846@lists.php.net to get a copy of this message
Pull Request: https://github.com/php/php-src/pull/24666 Author: zeynallow Profiling json_validate() on string-heavy input showed ~63% of the time in php_json_scan(): the first string pass (STR_P1) matches every byte as its own re2c token. json_validate() taking almost as long as json_decode() confirms the scan, not value construction, dominates. On the encoding side, json_encode($v, JSON_UNESCAPED_UNICODE) of non-ASCII text was ~3x slower than of ASCII text: for every non-ASCII character php_json_escape_string() flushes the pending run, decodes the character with php_next_utf8_char() and appends it on its own. Changes: - **Scanner:** before (re)entering STR_P1, skip the run of printable ASCII bytes other than " and \, 16 bytes at a time with SSE2 (baseline on x86_64) and bytewise otherwise. The re2c rules are unchanged; they are just entered less often. - **Encoder:** skip runs of bytes that are always copied verbatim (the same set the existing charmap lets through), and with JSON_UNESCAPED_UNICODE keep well-formed UTF-8 sequences in the pending run instead of copying them one by one. Validation matches php_next_utf8_char() (no overlongs, surrogates or code points above U+10FFFF), and U+2028/U+2029 are only kept when JSON_UNESCAPED_LINE_TERMINATORS is set. Everything else falls through to the existing code, so output and errors are unchanged. ### Results Release build (-O2), x86_64, median of 5 interleaved runs, 20 000-element payloads: | payload | operation | before | after | speedup | |---|---|---:|---:|---:| | ASCII text (11 MB) | json_decode | 27.07 ms | 10.39 ms | 2.60x | | | json_validate | 24.94 ms | 8.62 ms | 2.90x | | | json_encode | 18.91 ms | 11.96 ms | 1.58x | | UTF-8 text (11 MB) | json_decode | 29.47 ms | 19.33 ms | 1.52x | | | json_encode | 67.08 ms | 59.20 ms | 1.13x | | | json_encode + JSON_UNESCAPED_UNICODE | 62.20 ms | 30.36 ms | 2.05x | | strings with escapes (4 MB) | json_decode | 17.07 ms | 13.86 ms | 1.23x | | | json_encode | 15.34 ms | 10.62 ms | 1.44x | | short mixed values (3.7 MB) | json_decode | 18.87 ms | 17.92 ms | 1.05x | | numbers only (1.7 MB) | json_decode | 16.70 ms | 16.84 ms | 0.99x (noise) | Without SSE2 (scalar fallback only, e.g. aarch64) the gains are smaller: ASCII decode 1.78x, UTF-8 encode with JSON_UNESCAPED_UNICODE 1.71x, but decoding escape-heavy strings is ~5% slower. A NEON variant of the skip loop could follow if wanted. ### Testing - New ext/json/tests/string_scan_boundaries.phpt puts each special byte (control characters, invalid UTF-8, quotes, escapes, U+2028, ...) at every position of strings of length 0–48, across 16-byte chunk boundaries, for decode, validate and encode with several flag sets. It passes before and after this change, and caught an off-by-one in an earlier version of the SIMD mask. - Differential fuzzing against an unpatched binary: 1 000 000 json_decode() calls (4 flag sets) plus json_validate() on random inputs, and 1 440 000 json_encode() calls (12 flag sets), all identical including errors. Also with the SIMD paths disabled. - ext/json tests pass on release and debug builds, with and without the SIMD paths; no valgrind errors.

« previous php.git-pulls (#39846) next »