ext/json: replace packed charmap bitmap with a byte-indexed lookup table - #23682
ext/json: replace packed charmap bitmap with a byte-indexed lookup table#23682adapik wants to merge 3 commits into
Conversation
|
|
||
| static const char digits[] = "0123456789abcdef"; | ||
|
|
||
| static uint8_t php_json_escape_dirty_table[256]; |
There was a problem hiding this comment.
This could be a const static table of bool, we have other such static tables in ext/mbstring.
Might make sense to have a small PHP file generate a C header file that contains just the table. :)
|
I actually started with a hand-written static map, but found it hard to read and not self-explanatory. Now I followed the well-established ext/mbstring pattern instead - script ext/json/gen_json_escape_table.php generates php_json_escape_table.h (a static const bool[256] table). @Girgias thanks for the tip-off Benchmarks did'nt move (obviously). |
I believe that by using designated initializers and comments we can get the best of both worlds: Readability and no moving parts (the generated file should be set up with https://github.com/php/php-src/blob/master/.github/actions/verify-generated-files/action.yml and the Makefile). Inverting the meaning of the entries and then doing the following: will safely initialize the remaining entries to Side note: Should 0x7f really not be escaped? That looks like a pre-existing bug. |
|
@TimWolla Good point, but I don't think this needs the CI/Makefile editing. The table content is fixed (it encodes JSON's escaping rules, which aren't expected to change), so For what it's worth, the mbstring generators this PR follows ( DEL ( |
That would be a bug then. The CI job exists to verify generated files actually are in sync (to prevent accidental or malicious changes from slipping into generated files).
Surprising (given that it's practically non-printable), but it seems that node.js agrees, so no change needed. |
|
@TimWolla No worries, not against it in general - just figured it wasn't needed since the table's static. But yeah, fair point about what the CI job's actually for, so I went ahead and added it. I wouldn't mind going through the rest of the scripts and opening a follow-up PR for those later on. I wouldn't trust nodejs :) just saying, only stick to RFC |
53fd066 to
5114f86
Compare
Summary
php_json_escape_string()'s scalar loop tests every byte of every string withZEND_BIT_TEST(charmap, byte)— a packed 256-bit bitmap decoded ascharmap[byte/32] >> (byte%32) & 1. That's a real dependency chain: compute a shift+mask to get the word index, then load, then shift the loaded word to extract the bit. This runs on every character passed tojson_encode(), regardless of content.This PR replaces it with a direct 256-byte lookup table indexed by the byte value itself —
table[byte]. No address computation needed before the load; the byte value is the index.How this was found
While investigating a SIMD fast-path #23675 for the same function, I found this classification test wasn't part of the SIMD work at all — it's the pre-existing scalar loop every build already runs, untouched since long before any vectorization. That made it worth optimizing on its own, independent of any SIMD story, since it benefits every build on every architecture.
My first version generated the 256-entry table at compile time via a couple of helper macros expanding a readable predicate into a
static constinitializer. It worked and benchmarked identically, but I found the macro too ugly and verbose for what it bought. In this PR we fill the table with a single, ordinaryforloop, called once fromPHP_MINIT_FUNCTION(json), it's much easy to understand, test and process. It runs once per process, before any request — the overhead is one 256-iteration loop at startup, which is negligible against the millions ofjson_encode()calls it then speeds up over the process's lifetime.Benchmark
Docker, AMD Ryzen 5 7500F, gcc 13.3, default release flags), 7 trials/corpus,
mastervs. this change:No regression on any corpus beyond two 4–5 byte boundary cases at −5%. Every substantive corpus improves, including non-ASCII and URL-heavy content, which are typically the hardest to move without a trade-off elsewhere. Biggest win: +18% on
mixedcontent (medium/large).I used the same test suite as in #23675.
Reproducing these numbers
Testing
UNESCAPED_SLASHES|UNESCAPED_UNICODE|HEX_*) configurations.charmapbitmap for all 256 byte values.json_encode_byte_classification.phpt) sweeps all 256 byte values against an independently-written reference implementation of the escaping rules, across four option combinations, plus verifies every lone high-bit byte correctly reportsJSON_ERROR_UTF8.