You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
So 5x speedup on SSE2 and 9.9x speedup on SSE4.2. Not quite 20x (which would not be possible with SSE anyway because those vectors are 16 byte elements and you also have to account for the overhead); but still some win.
Maybe there's some obvious trick I'm missing in my patch especially regarding SSE2. Note also that this could maybe cause a slowdown when you have a lot of special characters next to each other that need escaping. Maybe there are also some articles about fast json encoding.
EDIT: by getting rid of the tail variable it's down to 6.94ms for SSE4.2
Reacted by Christoph M. Becker, Michael Voříšek, Arnaud Le Blanc and Patrick
I also did a test with an adverserial input. The absolute worst case is a very long string containing only escape characters (or all non-printable ASCII). Because in that case, we have no benefit from the SIMD but do have its overhead.
Both the SSE2 & SSE4.2 have a comparable slowdown compared to not using SIMD in that case: 3.25 ± 0.06 times slower...
I belive there is a potential to utilize SIMD to copy multiple characters to the output string as long as they are not to-be-escaped.
The copying itself happens via memcpy, which is optimized like crazy by the C library already. Still, this takes a lot of time as well but we can't really go around that unless we start doing some very advanced stuff like supporting rope strings everywhere, and even then we get the performance penalty once we need to materialize the rope.
By the way, I still believe there is some general potential to improve the performance of smart_str(ing) by increasing by a factor (instead of increasing by a summand). I might be wrong, though.
Wouldn't affect this a lot here because we preallocate a buffer equal to the input length (well unless there's a lot of escaping, then it does matter).
But indeed, in general, using a factor should lead to a lower amortized time complexity. And ideally a factor lower than the golden ratio (e.g. something like 1.5) to have better memory chunk reusage.
We do increase per page though so the effect may only happen on very large string builds:
The speedup you nailed is awesome 😍 and will greatly improve apps that serve large blobs in JSON. Thank you!
slowdown
If I understand the impl. well, the computed "mask" is currently discarded once "to-be-escaped" character is found. What about reusing the computed knowledge for the remaining characters?
Indeed. What happens now is that we just move the "pos" to the first character that must be escaped.
In theory, we know based on the set bits in the "mask" which characters need escaping. How to leverage this efficiently is something I'd have to think about more sometime in the upcoming week.
By reusing the mask information I got the overhead of the worse case down to about 2.07x.
Development PR here if anyone's interested: ndossche#120, still quite in a WIP state.
Reacted by Christoph M. Becker
Reacted by Ayesh Karunaratne and Denny Septian Panggabean
Activity
cmb69 commentedon Feb 2, 2025
There is already https://pecl.php.net/package/simdjson.
mvorisek commentedon Feb 2, 2025
That lib is for decoding JSON. This feature request is about encoding.
cmb69 commentedon Feb 2, 2025
Oh, right.
ndossche commentedon Feb 2, 2025
I played a bit with this, and have this patch: https://gist.github.com/nielsdos/3b42ffaa4476bb5cb7eb498935072312 (somewhat dirty, would need some more work)
Without patch:
Naive SSE2:
Semi-naive SSE4.2:
So 5x speedup on SSE2 and 9.9x speedup on SSE4.2. Not quite 20x (which would not be possible with SSE anyway because those vectors are 16 byte elements and you also have to account for the overhead); but still some win.
Maybe there's some obvious trick I'm missing in my patch especially regarding SSE2. Note also that this could maybe cause a slowdown when you have a lot of special characters next to each other that need escaping. Maybe there are also some articles about fast json encoding.
EDIT: by getting rid of the tail variable it's down to 6.94ms for SSE4.2
ndossche commentedon Feb 2, 2025
After further simplifying my patch (getting rid of tail handling and simplifying the range check)
I arrive at:
SSE4.2:
SSE2:
ndossche commentedon Feb 2, 2025
I also did a test with an adverserial input. The absolute worst case is a very long string containing only escape characters (or all non-printable ASCII). Because in that case, we have no benefit from the SIMD but do have its overhead.
Both the SSE2 & SSE4.2 have a comparable slowdown compared to not using SIMD in that case:
3.25 ± 0.06 timesslower...Test script for adverserial input:
Ran using hyperfine.
ndossche commentedon Feb 2, 2025
BTW
The copying itself happens via memcpy, which is optimized like crazy by the C library already. Still, this takes a lot of time as well but we can't really go around that unless we start doing some very advanced stuff like supporting rope strings everywhere, and even then we get the performance penalty once we need to materialize the rope.
cmb69 commentedon Feb 2, 2025
By the way, I still believe there is some general potential to improve the performance of
smart_str(ing)by increasing by a factor (instead of increasing by a summand). I might be wrong, though.ndossche commentedon Feb 2, 2025
Wouldn't affect this a lot here because we preallocate a buffer equal to the input length (well unless there's a lot of escaping, then it does matter).
But indeed, in general, using a factor should lead to a lower amortized time complexity. And ideally a factor lower than the golden ratio (e.g. something like 1.5) to have better memory chunk reusage.
We do increase per page though so the effect may only happen on very large string builds:
php-src/Zend/zend_smart_str.c
Lines 27 to 35 in 6ae1209
mvorisek commentedon Feb 2, 2025
The speedup you nailed is awesome 😍 and will greatly improve apps that serve large blobs in JSON. Thank you!
If I understand the impl. well, the computed "
mask" is currently discarded once "to-be-escaped" character is found. What about reusing the computed knowledge for the remaining characters?ndossche commentedon Feb 2, 2025
Indeed. What happens now is that we just move the "pos" to the first character that must be escaped.
In theory, we know based on the set bits in the "mask" which characters need escaping. How to leverage this efficiently is something I'd have to think about more sometime in the upcoming week.
ndossche commentedon Feb 3, 2025
By reusing the mask information I got the overhead of the worse case down to about 2.07x.
Development PR here if anyone's interested: ndossche#120, still quite in a WIP state.
xperia-query commentedon Nov 17, 2025
Asian(CJK , Vietnamese .. etc) strings use UTF8, so escape characters are added.
So, I always need to add the UTF8 (JSON_UNESCAPED_UNICODE) option. Will this still improve performance?