Skip to content

ext/json: Speed up scanning and escaping of strings - #24666

Open
zeynallow wants to merge 1 commit into
php:masterfrom
zeynallow:json-string-fast-paths
Open

zeynallow wants to merge 1 commit into
php:masterfrom
zeynallow:json-string-fast-paths

Conversation

@zeynallow

Copy link
Copy Markdown

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.

The first scanner pass matched every byte of a string as a separate re2c
token, and json_encode() copied the pending run each time it met a byte
that is not plain ASCII, then decoded the character with
php_next_utf8_char() and copied it on its own.

- Scanner: before (re)entering the first string pass, skip the run of
  printable ASCII bytes other than quote and backslash, 16 bytes at a
  time with SSE2 and bytewise otherwise.
- Encoder: skip runs of bytes that are always copied verbatim the same
  way, and with JSON_UNESCAPED_UNICODE keep well-formed UTF-8 sequences
  in the pending run. Anything else still takes the existing code path,
  so the output is unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant