Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 860b11f

Browse files
committedApr 18, 2025··
Optimize JSON string encoding
There are a couple of optimizations that work together: - We now use the specialized php_next_utf8_char_mb() helper function to avoid pressure on the µop and instruction cache. - It no longer emits UTF-8 bytes under PHP_JSON_UNESCAPED_UNICODE until it actually has to. By emitting in bulk, this improves performance. - Code layout tweaks * Use a specialized php_json_append() and assertions to avoid allocating the initial buffer, as this is already done upfront. * Factor out the call to smart_str_extend() to above the UTF-16 check to avoid code bloat. - Use SIMD, either with SSE2 or SSE4.2. A resolver is used when SSE4.2 is not configured at compile time.
1 parent c3d7610 commit 860b11f

File tree

2 files changed

+317
-89
lines changed

2 files changed

+317
-89
lines changed
 

‎UPGRADING

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,9 @@ PHP 8.5 UPGRADE NOTES
457457
14. Performance Improvements
458458
========================================
459459

460+
- JSON:
461+
. Encoding JSON strings without special characters is now faster.
462+
460463
- ReflectionProperty:
461464
. Improved performance of the following methods: getValue(), getRawValue(),
462465
isInitialized(), setValue(), setRawValue().

‎ext/json/json_encoder.c

Lines changed: 314 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,260 @@
2929
#include "zend_enum.h"
3030
#include "zend_property_hooks.h"
3131
#include "zend_lazy_objects.h"
32+
#include "zend_bitset.h"
33+
34+
#if defined(ZEND_INTRIN_SSE4_2_NATIVE) || defined(ZEND_INTRIN_SSE4_2_FUNC_PROTO)
35+
# include <nmmintrin.h>
36+
#endif
37+
#ifdef ZEND_INTRIN_SSE4_2_FUNC_PROTO
38+
# include "zend_cpuinfo.h"
39+
#endif
40+
41+
#ifdef __SSE2__
42+
# define JSON_USE_SIMD
43+
#endif
44+
45+
typedef enum php_json_simd_result {
46+
PHP_JSON_STOP,
47+
PHP_JSON_SLOW,
48+
PHP_JSON_NON_ASCII,
49+
} php_json_simd_result;
50+
51+
/* Specialization of smart_str_appendl() to avoid performance loss due to code bloat */
52+
static zend_always_inline void php_json_append(smart_str *dest, const char *src, size_t len)
53+
{
54+
/* dest has a minimum size of the input length,
55+
* this avoids generating initial allocation code */
56+
ZEND_ASSERT(dest->s);
57+
58+
smart_str_appendl(dest, src, len);
59+
}
60+
61+
static zend_always_inline bool php_json_printable_ascii_escape(smart_str *buf, unsigned char us, int options)
62+
{
63+
ZEND_ASSERT(buf->s);
64+
65+
switch (us) {
66+
case '"':
67+
if (options & PHP_JSON_HEX_QUOT) {
68+
php_json_append(buf, "\\u0022", 6);
69+
} else {
70+
php_json_append(buf, "\\\"", 2);
71+
}
72+
break;
73+
74+
case '\\':
75+
php_json_append(buf, "\\\\", 2);
76+
break;
77+
78+
case '/':
79+
if (options & PHP_JSON_UNESCAPED_SLASHES) {
80+
smart_str_appendc(buf, '/');
81+
} else {
82+
php_json_append(buf, "\\/", 2);
83+
}
84+
break;
85+
86+
case '<':
87+
if (options & PHP_JSON_HEX_TAG) {
88+
php_json_append(buf, "\\u003C", 6);
89+
} else {
90+
smart_str_appendc(buf, '<');
91+
}
92+
break;
93+
94+
case '>':
95+
if (options & PHP_JSON_HEX_TAG) {
96+
php_json_append(buf, "\\u003E", 6);
97+
} else {
98+
smart_str_appendc(buf, '>');
99+
}
100+
break;
101+
102+
case '&':
103+
if (options & PHP_JSON_HEX_AMP) {
104+
php_json_append(buf, "\\u0026", 6);
105+
} else {
106+
smart_str_appendc(buf, '&');
107+
}
108+
break;
109+
110+
case '\'':
111+
if (options & PHP_JSON_HEX_APOS) {
112+
php_json_append(buf, "\\u0027", 6);
113+
} else {
114+
smart_str_appendc(buf, '\'');
115+
}
116+
break;
117+
118+
default:
119+
return false;
120+
}
121+
122+
return true;
123+
}
124+
125+
#ifdef JSON_USE_SIMD
126+
static zend_always_inline int php_json_sse2_compute_escape_intersection(const __m128i mask, const __m128i input)
127+
{
128+
(void) mask;
129+
130+
const __m128i result_34 = _mm_cmpeq_epi8(input, _mm_set1_epi8('"'));
131+
const __m128i result_38 = _mm_cmpeq_epi8(input, _mm_set1_epi8('&'));
132+
const __m128i result_39 = _mm_cmpeq_epi8(input, _mm_set1_epi8('\''));
133+
const __m128i result_47 = _mm_cmpeq_epi8(input, _mm_set1_epi8('/'));
134+
const __m128i result_60 = _mm_cmpeq_epi8(input, _mm_set1_epi8('<'));
135+
const __m128i result_62 = _mm_cmpeq_epi8(input, _mm_set1_epi8('>'));
136+
const __m128i result_92 = _mm_cmpeq_epi8(input, _mm_set1_epi8('\\'));
137+
138+
const __m128i result_34_38 = _mm_or_si128(result_34, result_38);
139+
const __m128i result_39_47 = _mm_or_si128(result_39, result_47);
140+
const __m128i result_60_62 = _mm_or_si128(result_60, result_62);
141+
142+
const __m128i result_34_38_39_47 = _mm_or_si128(result_34_38, result_39_47);
143+
const __m128i result_60_62_92 = _mm_or_si128(result_60_62, result_92);
144+
145+
const __m128i result_individual_bytes = _mm_or_si128(result_34_38_39_47, result_60_62_92);
146+
return _mm_movemask_epi8(result_individual_bytes);
147+
}
148+
149+
#if defined(ZEND_INTRIN_SSE4_2_NATIVE) || defined(ZEND_INTRIN_SSE4_2_FUNC_PROTO)
150+
static const char php_json_escape_noslashes_lut[2][8][16] = {
151+
/* !PHP_JSON_UNESCAPED_SLASHES */
152+
{
153+
[0] = {'"', '\\', '/', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
154+
[PHP_JSON_HEX_AMP] = {'"', '\\', '&', '/', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
155+
[PHP_JSON_HEX_APOS] = {'"', '\\', '\'', '/', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
156+
[PHP_JSON_HEX_AMP|PHP_JSON_HEX_APOS] = {'"', '\\', '&', '\'', '/', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
157+
[PHP_JSON_HEX_TAG] = {'"', '\\', '<', '>', '/', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
158+
[PHP_JSON_HEX_AMP|PHP_JSON_HEX_TAG] = {'"', '\\', '&', '<', '>', '/', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
159+
[PHP_JSON_HEX_APOS|PHP_JSON_HEX_TAG] = {'"', '\\', '\'', '<', '>', '/', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
160+
[PHP_JSON_HEX_AMP|PHP_JSON_HEX_APOS|PHP_JSON_HEX_TAG] = {'"', '\\', '&', '\'', '<', '>', '/', 0, 0, 0, 0, 0, 0, 0, 0, 0}
161+
},
162+
163+
/* PHP_JSON_UNESCAPED_SLASHES */
164+
{
165+
[0] = {'"', '\\', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
166+
[PHP_JSON_HEX_AMP] = {'"', '\\', '&', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
167+
[PHP_JSON_HEX_APOS] = {'"', '\\', '\'', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
168+
[PHP_JSON_HEX_AMP|PHP_JSON_HEX_APOS] = {'"', '\\', '&', '\'', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
169+
[PHP_JSON_HEX_TAG] = {'"', '\\', '<', '>', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
170+
[PHP_JSON_HEX_AMP|PHP_JSON_HEX_TAG] = {'"', '\\', '&', '<', '>', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
171+
[PHP_JSON_HEX_APOS|PHP_JSON_HEX_TAG] = {'"', '\\', '\'', '<', '>', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
172+
[PHP_JSON_HEX_AMP|PHP_JSON_HEX_APOS|PHP_JSON_HEX_TAG] = {'"', '\\', '&', '\'', '<', '>', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
173+
}
174+
};
175+
176+
static zend_always_inline __m128i php_json_create_sse_escape_mask(int options)
177+
{
178+
const int slashes = (options & PHP_JSON_UNESCAPED_SLASHES) ? 1 : 0;
179+
const int masked = options & (PHP_JSON_HEX_AMP|PHP_JSON_HEX_APOS|PHP_JSON_HEX_TAG);
180+
return *(const __m128i *) &php_json_escape_noslashes_lut[slashes][masked];
181+
}
182+
183+
ZEND_INTRIN_SSE4_2_FUNC_DECL(int php_json_sse42_compute_escape_intersection_real(const __m128i mask, const __m128i input));
184+
zend_always_inline int php_json_sse42_compute_escape_intersection_real(const __m128i mask, const __m128i input)
185+
{
186+
const __m128i result_individual_bytes = _mm_cmpistrm(mask, input, _SIDD_SBYTE_OPS | _SIDD_CMP_EQUAL_ANY | _SIDD_BIT_MASK);
187+
return _mm_cvtsi128_si32(result_individual_bytes);
188+
}
189+
#endif
190+
191+
#ifdef ZEND_INTRIN_SSE4_2_FUNC_PROTO
192+
static int php_json_sse42_compute_escape_intersection(const __m128i mask, const __m128i input) __attribute__((ifunc("php_json_resolve_escape_intersection")));
193+
194+
typedef int (*php_json_compute_escape_intersection_t)(const __m128i mask, const __m128i input);
195+
196+
ZEND_NO_SANITIZE_ADDRESS
197+
ZEND_ATTRIBUTE_UNUSED /* clang mistakenly warns about this */
198+
static php_json_compute_escape_intersection_t php_json_resolve_escape_intersection(void) {
199+
if (zend_cpu_supports_sse42()) {
200+
return php_json_sse42_compute_escape_intersection_real;
201+
}
202+
return php_json_sse2_compute_escape_intersection;
203+
}
204+
#endif
205+
206+
static zend_always_inline php_json_simd_result php_json_process_simd_block(
207+
smart_str *buf,
208+
const __m128i sse_escape_mask,
209+
const char **restrict s,
210+
size_t *restrict pos,
211+
size_t *restrict len,
212+
int options
213+
)
214+
{
215+
while (*len >= sizeof(__m128i)) {
216+
const __m128i input = _mm_loadu_si128((const __m128i *) (*s + *pos));
217+
/* signed compare, so checks for unsigned bytes >= 0x80 as well */
218+
const __m128i input_range = _mm_cmplt_epi8(input, _mm_set1_epi8(32));
219+
220+
int max_shift = sizeof(__m128i);
221+
222+
int input_range_mask = _mm_movemask_epi8(input_range);
223+
if (input_range_mask != 0) {
224+
if (UNEXPECTED(input_range_mask & 1)) {
225+
/* not worth it */
226+
return PHP_JSON_NON_ASCII;
227+
}
228+
max_shift = zend_ulong_ntz(input_range_mask);
229+
}
230+
231+
#ifdef ZEND_INTRIN_SSE4_2_NATIVE
232+
int mask = php_json_sse42_compute_escape_intersection_real(sse_escape_mask, input);
233+
#elif defined(ZEND_INTRIN_SSE4_2_FUNC_PROTO)
234+
int mask = php_json_sse42_compute_escape_intersection(sse_escape_mask, input);
235+
#else
236+
int mask = php_json_sse2_compute_escape_intersection(_mm_setzero_si128(), input);
237+
#endif
238+
if (mask != 0) {
239+
if (UNEXPECTED(max_shift < sizeof(__m128i))) {
240+
int shift = zend_ulong_ntz(mask); /* first offending character */
241+
*pos += MIN(max_shift, shift);
242+
*len -= MIN(max_shift, shift);
243+
return PHP_JSON_SLOW;
244+
}
245+
246+
php_json_append(buf, *s, *pos);
247+
*s += *pos;
248+
const char *s_backup = *s;
249+
250+
/* It's more important to keep this loop tight than to optimize this with
251+
* a trailing zero count. */
252+
for (; mask; mask >>= 1, *s += 1) {
253+
if (UNEXPECTED(mask & 1)) {
254+
bool handled = php_json_printable_ascii_escape(buf, (*s)[0], options);
255+
ZEND_ASSERT(handled);
256+
} else {
257+
ZEND_ASSERT(buf->s);
258+
smart_str_appendc(buf, (*s)[0]);
259+
}
260+
}
261+
262+
*pos = sizeof(__m128i) - (*s - s_backup);
263+
} else {
264+
if (max_shift < sizeof(__m128i)) {
265+
*pos += max_shift;
266+
*len -= max_shift;
267+
return PHP_JSON_SLOW;
268+
}
269+
*pos += sizeof(__m128i);
270+
}
271+
272+
*len -= sizeof(__m128i);
273+
}
274+
275+
return UNEXPECTED(!*len) ? PHP_JSON_STOP : PHP_JSON_SLOW;
276+
}
277+
278+
# if defined(ZEND_INTRIN_SSE4_2_NATIVE) || defined(ZEND_INTRIN_SSE4_2_FUNC_PROTO)
279+
# define JSON_DEFINE_ESCAPE_MASK(name, options) const __m128i name = php_json_create_sse_escape_mask(options)
280+
# else
281+
# define JSON_DEFINE_ESCAPE_MASK(name, options) const __m128i name = _mm_setzero_si128()
282+
# endif
283+
#else
284+
# define JSON_DEFINE_ESCAPE_MASK(name, options)
285+
#endif
32286

33287
static const char digits[] = "0123456789abcdef";
34288

@@ -394,54 +648,64 @@ zend_result php_json_escape_string(
394648
}
395649

396650
}
397-
checkpoint = buf->s ? ZSTR_LEN(buf->s) : 0;
398651

399652
/* pre-allocate for string length plus 2 quotes */
400653
smart_str_alloc(buf, len+2, 0);
654+
checkpoint = ZSTR_LEN(buf->s);
401655
smart_str_appendc(buf, '"');
402656

403657
pos = 0;
404658

659+
JSON_DEFINE_ESCAPE_MASK(sse_escape_mask, options);
660+
405661
do {
406662
static const uint32_t charmap[8] = {
407663
0xffffffff, 0x500080c4, 0x10000000, 0x00000000,
408664
0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff};
409665

666+
php_json_simd_result result = PHP_JSON_SLOW;
667+
#ifdef JSON_USE_SIMD
668+
result = php_json_process_simd_block(buf, sse_escape_mask, &s, &pos, &len, options);
669+
if (UNEXPECTED(result == PHP_JSON_STOP)) {
670+
break;
671+
}
672+
#endif
673+
410674
us = (unsigned char)s[pos];
411-
if (EXPECTED(!ZEND_BIT_TEST(charmap, us))) {
675+
if (EXPECTED(result != PHP_JSON_NON_ASCII && !ZEND_BIT_TEST(charmap, us))) {
412676
pos++;
413677
len--;
414-
if (len == 0) {
415-
smart_str_appendl(buf, s, pos);
416-
break;
417-
}
418678
} else {
419-
if (pos) {
420-
smart_str_appendl(buf, s, pos);
421-
s += pos;
422-
pos = 0;
423-
}
424-
us = (unsigned char)s[0];
425679
if (UNEXPECTED(us >= 0x80)) {
426-
zend_result status;
427-
us = php_next_utf8_char((unsigned char *)s, len, &pos, &status);
680+
size_t pos_old = pos;
681+
const char *cur = s + pos;
682+
pos = 0;
683+
us = php_next_utf8_char_mb((unsigned char *)cur, us, len, &pos);
684+
len -= pos;
685+
pos += pos_old;
428686

429687
/* check whether UTF8 character is correct */
430-
if (UNEXPECTED(status != SUCCESS)) {
688+
if (UNEXPECTED(!us)) {
689+
if (pos_old && (options & (PHP_JSON_INVALID_UTF8_IGNORE|PHP_JSON_INVALID_UTF8_SUBSTITUTE))) {
690+
php_json_append(buf, s, pos_old);
691+
}
692+
s += pos;
693+
pos = 0;
694+
431695
if (options & PHP_JSON_INVALID_UTF8_IGNORE) {
432696
/* ignore invalid UTF8 character */
433697
} else if (options & PHP_JSON_INVALID_UTF8_SUBSTITUTE) {
434698
/* Use Unicode character 'REPLACEMENT CHARACTER' (U+FFFD) */
435699
if (options & PHP_JSON_UNESCAPED_UNICODE) {
436-
smart_str_appendl(buf, "\xef\xbf\xbd", 3);
700+
php_json_append(buf, "\xef\xbf\xbd", 3);
437701
} else {
438-
smart_str_appendl(buf, "\\ufffd", 6);
702+
php_json_append(buf, "\\ufffd", 6);
439703
}
440704
} else {
441705
ZSTR_LEN(buf->s) = checkpoint;
442706
encoder->error_code = PHP_JSON_ERROR_UTF8;
443707
if (options & PHP_JSON_PARTIAL_OUTPUT_ON_ERROR) {
444-
smart_str_appendl(buf, "null", 4);
708+
php_json_append(buf, "null", 4);
445709
}
446710
return FAILURE;
447711
}
@@ -452,126 +716,87 @@ zend_result php_json_escape_string(
452716
} else if ((options & PHP_JSON_UNESCAPED_UNICODE)
453717
&& ((options & PHP_JSON_UNESCAPED_LINE_TERMINATORS)
454718
|| us < 0x2028 || us > 0x2029)) {
455-
smart_str_appendl(buf, s, pos);
719+
/* No need to emit any bytes, just move the cursor. */
456720
} else {
721+
php_json_append(buf, s, pos_old);
722+
s += pos;
723+
pos = 0;
724+
725+
ZEND_ASSERT(buf->s);
726+
457727
/* From http://en.wikipedia.org/wiki/UTF16 */
728+
dst = smart_str_extend(buf, 6 + ((us >= 0x10000) ? 6 : 0));
458729
if (us >= 0x10000) {
459730
unsigned int next_us;
460731

461732
us -= 0x10000;
462733
next_us = (unsigned short)((us & 0x3ff) | 0xdc00);
463734
us = (unsigned short)((us >> 10) | 0xd800);
464-
dst = smart_str_extend(buf, 6);
465735
dst[0] = '\\';
466736
dst[1] = 'u';
467737
dst[2] = digits[(us >> 12) & 0xf];
468738
dst[3] = digits[(us >> 8) & 0xf];
469739
dst[4] = digits[(us >> 4) & 0xf];
470740
dst[5] = digits[us & 0xf];
471741
us = next_us;
742+
dst += 6;
472743
}
473-
dst = smart_str_extend(buf, 6);
474744
dst[0] = '\\';
475745
dst[1] = 'u';
476746
dst[2] = digits[(us >> 12) & 0xf];
477747
dst[3] = digits[(us >> 8) & 0xf];
478748
dst[4] = digits[(us >> 4) & 0xf];
479749
dst[5] = digits[us & 0xf];
480750
}
481-
s += pos;
482-
len -= pos;
483-
pos = 0;
484751
} else {
752+
if (pos) {
753+
php_json_append(buf, s, pos);
754+
s += pos;
755+
pos = 0;
756+
}
485757
s++;
486758
switch (us) {
487-
case '"':
488-
if (options & PHP_JSON_HEX_QUOT) {
489-
smart_str_appendl(buf, "\\u0022", 6);
490-
} else {
491-
smart_str_appendl(buf, "\\\"", 2);
492-
}
493-
break;
494-
495-
case '\\':
496-
smart_str_appendl(buf, "\\\\", 2);
497-
break;
498-
499-
case '/':
500-
if (options & PHP_JSON_UNESCAPED_SLASHES) {
501-
smart_str_appendc(buf, '/');
502-
} else {
503-
smart_str_appendl(buf, "\\/", 2);
504-
}
505-
break;
506-
507759
case '\b':
508-
smart_str_appendl(buf, "\\b", 2);
760+
php_json_append(buf, "\\b", 2);
509761
break;
510762

511763
case '\f':
512-
smart_str_appendl(buf, "\\f", 2);
764+
php_json_append(buf, "\\f", 2);
513765
break;
514766

515767
case '\n':
516-
smart_str_appendl(buf, "\\n", 2);
768+
php_json_append(buf, "\\n", 2);
517769
break;
518770

519771
case '\r':
520-
smart_str_appendl(buf, "\\r", 2);
772+
php_json_append(buf, "\\r", 2);
521773
break;
522774

523775
case '\t':
524-
smart_str_appendl(buf, "\\t", 2);
525-
break;
526-
527-
case '<':
528-
if (options & PHP_JSON_HEX_TAG) {
529-
smart_str_appendl(buf, "\\u003C", 6);
530-
} else {
531-
smart_str_appendc(buf, '<');
532-
}
533-
break;
534-
535-
case '>':
536-
if (options & PHP_JSON_HEX_TAG) {
537-
smart_str_appendl(buf, "\\u003E", 6);
538-
} else {
539-
smart_str_appendc(buf, '>');
540-
}
541-
break;
542-
543-
case '&':
544-
if (options & PHP_JSON_HEX_AMP) {
545-
smart_str_appendl(buf, "\\u0026", 6);
546-
} else {
547-
smart_str_appendc(buf, '&');
548-
}
549-
break;
550-
551-
case '\'':
552-
if (options & PHP_JSON_HEX_APOS) {
553-
smart_str_appendl(buf, "\\u0027", 6);
554-
} else {
555-
smart_str_appendc(buf, '\'');
556-
}
776+
php_json_append(buf, "\\t", 2);
557777
break;
558778

559779
default:
560-
ZEND_ASSERT(us < ' ');
561-
dst = smart_str_extend(buf, 6);
562-
dst[0] = '\\';
563-
dst[1] = 'u';
564-
dst[2] = '0';
565-
dst[3] = '0';
566-
dst[4] = digits[(us >> 4) & 0xf];
567-
dst[5] = digits[us & 0xf];
780+
if (!php_json_printable_ascii_escape(buf, us, options)) {
781+
ZEND_ASSERT(us < ' ');
782+
dst = smart_str_extend(buf, 6);
783+
dst[0] = '\\';
784+
dst[1] = 'u';
785+
dst[2] = '0';
786+
dst[3] = '0';
787+
dst[4] = digits[(us >> 4) & 0xf];
788+
dst[5] = digits[us & 0xf];
789+
}
568790
break;
569791
}
570792
len--;
571793
}
572794
}
573795
} while (len);
574796

797+
php_json_append(buf, s, pos);
798+
799+
ZEND_ASSERT(buf->s);
575800
smart_str_appendc(buf, '"');
576801

577802
return SUCCESS;

0 commit comments

Comments
 (0)
Please sign in to comment.