[php-src] PR #23682: json: replace packed charmap bitmap with a byte-indexed lookup table

From: Date: Sun, 13 Sep 2026 17:45:38 +0000
Subject: [php-src] PR #23682: json: replace packed charmap bitmap with a byte-indexed lookup table
Groups: php.git-pulls 
Request: Send a blank email to git-pulls+get-38472@lists.php.net to get a copy of this message
Pull Request: https://github.com/php/php-src/pull/23682 Author: adapik ## Summary php_json_escape_string()'s scalar loop tests every byte of every string with ZEND_BIT_TEST(charmap, byte) — a packed 256-bit bitmap decoded as charmap[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 to json_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 const initializer. 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, ordinary for loop, called once from PHP_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 of json_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, master vs. this change: | Corpus | small | medium | large | |---|---|---|---| | clean_ascii | +8% | +10% | +6% | | html_heavy | +3% | +4% | +5% | | mixed | +3% | +18% | +18% | | non_ascii | +5% | +6% | +4% | | url_heavy | +1% | +1% | +6% | 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 mixed content (medium/large). I used the same test suite as in #23675. ## Testing - 30/30 corpora pass byte-for-byte correctness against unpatched output, across both default and full-flags (UNESCAPED_SLASHES|UNESCAPED_UNICODE|HEX_*) configurations. - Table contents independently verified bit-exact against the original charmap bitmap for all 256 byte values. - New test (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 reports JSON_ERROR_UTF8. - No behavior change — output is identical for every input; this is purely an internal implementation change to an existing function.

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