Skip to content

Commit 78514e3

Browse files
authored
Re-use the Neon code between the generator and the parser (#6)
* Reuse the SIMD code between the generator and the parser.
1 parent 8746ede commit 78514e3

3 files changed

Lines changed: 184 additions & 140 deletions

File tree

ext/json/ext/generator/generator.c

Lines changed: 48 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -284,8 +284,29 @@ static inline FORCE_INLINE char *copy_remaining_bytes(search_state *search, unsi
284284
return s;
285285
}
286286

287+
static inline FORCE_INLINE int has_next_vector(void *state, size_t width)
288+
{
289+
search_state *search = (search_state *) state;
290+
return search->ptr + width <= search->end;
291+
}
292+
293+
static inline FORCE_INLINE const char *ptr(void *state)
294+
{
295+
return ((search_state *) state)->ptr;
296+
}
297+
298+
static inline FORCE_INLINE void advance_by(void *state, size_t count)
299+
{
300+
((search_state *) state)->ptr += count;
301+
}
302+
287303
#ifdef HAVE_SIMD_NEON
288304

305+
static inline FORCE_INLINE void set_match_mask(void *state, uint64_t mask)
306+
{
307+
((search_state *) state)->matches_mask = mask;
308+
}
309+
289310
static inline FORCE_INLINE unsigned char neon_next_match(search_state *search)
290311
{
291312
uint64_t mask = search->matches_mask;
@@ -304,20 +325,6 @@ static inline FORCE_INLINE unsigned char neon_next_match(search_state *search)
304325
return 1;
305326
}
306327

307-
static inline FORCE_INLINE uint64_t neon_rules_update(const char *ptr)
308-
{
309-
uint8x16_t chunk = vld1q_u8((const unsigned char *)ptr);
310-
311-
// Trick: c < 32 || c == 34 can be factored as c ^ 2 < 33
312-
// https://lemire.me/blog/2025/04/13/detect-control-characters-quotes-and-backslashes-efficiently-using-swar/
313-
const uint8x16_t too_low_or_dbl_quote = vcltq_u8(veorq_u8(chunk, vdupq_n_u8(2)), vdupq_n_u8(33));
314-
315-
uint8x16_t has_backslash = vceqq_u8(chunk, vdupq_n_u8('\\'));
316-
uint8x16_t needs_escape = vorrq_u8(too_low_or_dbl_quote, has_backslash);
317-
318-
return neon_match_mask(needs_escape);
319-
}
320-
321328
static inline unsigned char search_escape_basic_neon(search_state *search)
322329
{
323330
if (RB_UNLIKELY(search->has_matches)) {
@@ -372,26 +379,30 @@ static inline unsigned char search_escape_basic_neon(search_state *search)
372379
* no bytes need to be escaped and we can continue to the next chunk. If the mask is not 0 then we
373380
* have at least one byte that needs to be escaped.
374381
*/
375-
while (search->ptr + sizeof(uint8x16_t) <= search->end) {
376-
uint64_t mask = neon_rules_update(search->ptr);
377382

378-
if (!mask) {
379-
search->ptr += sizeof(uint8x16_t);
380-
continue;
381-
}
382-
search->matches_mask = mask;
383+
// Do not extract this structor to a global variable or make it static without profiling and
384+
// verifying that it does not cause performance regressions. clang 17 and gcc 14 currently
385+
// inline these methods if this structure is defined like this.
386+
NeonStringScanIterator iterator = {
387+
.has_next_vector = has_next_vector,
388+
.ptr = ptr,
389+
.advance_by = advance_by,
390+
.set_match_mask = set_match_mask
391+
};
392+
393+
if (string_scan_simd_neon(&iterator, search)) {
383394
search->has_matches = true;
384395
search->chunk_base = search->ptr;
385396
search->chunk_end = search->ptr + sizeof(uint8x16_t);
386397
return neon_next_match(search);
387398
}
388-
399+
389400
// There are fewer than 16 bytes left.
390401
unsigned long remaining = (search->end - search->ptr);
391402
if (remaining >= SIMD_MINIMUM_THRESHOLD) {
392403
char *s = copy_remaining_bytes(search, sizeof(uint8x16_t), remaining);
393404

394-
uint64_t mask = neon_rules_update(s);
405+
uint64_t mask = compute_chunk_mask_neon(s);
395406

396407
if (!mask) {
397408
// Nothing to escape, ensure search_flush doesn't do anything by setting
@@ -420,10 +431,11 @@ static inline unsigned char search_escape_basic_neon(search_state *search)
420431

421432
#ifdef HAVE_SIMD_SSE2
422433

423-
#define _mm_cmpge_epu8(a, b) _mm_cmpeq_epi8(_mm_max_epu8(a, b), a)
424-
#define _mm_cmple_epu8(a, b) _mm_cmpge_epu8(b, a)
425-
#define _mm_cmpgt_epu8(a, b) _mm_xor_si128(_mm_cmple_epu8(a, b), _mm_set1_epi8(-1))
426-
#define _mm_cmplt_epu8(a, b) _mm_cmpgt_epu8(b, a)
434+
static inline FORCE_INLINE void set_match_mask(void *state, int mask)
435+
{
436+
((search_state *) state)->matches_mask = mask;
437+
}
438+
427439

428440
static inline FORCE_INLINE unsigned char sse2_next_match(search_state *search)
429441
{
@@ -449,18 +461,6 @@ static inline FORCE_INLINE unsigned char sse2_next_match(search_state *search)
449461
#define TARGET_SSE2
450462
#endif
451463

452-
static inline TARGET_SSE2 FORCE_INLINE int sse2_update(const char *ptr)
453-
{
454-
__m128i chunk = _mm_loadu_si128((__m128i const*)ptr);
455-
456-
// Trick: c < 32 || c == 34 can be factored as c ^ 2 < 33
457-
// https://lemire.me/blog/2025/04/13/detect-control-characters-quotes-and-backslashes-efficiently-using-swar/
458-
__m128i too_low_or_dbl_quote = _mm_cmplt_epu8(_mm_xor_si128(chunk, _mm_set1_epi8(2)), _mm_set1_epi8(33));
459-
__m128i has_backslash = _mm_cmpeq_epi8(chunk, _mm_set1_epi8('\\'));
460-
__m128i needs_escape = _mm_or_si128(too_low_or_dbl_quote, has_backslash);
461-
return _mm_movemask_epi8(needs_escape);
462-
}
463-
464464
static inline TARGET_SSE2 FORCE_INLINE unsigned char search_escape_basic_sse2(search_state *search)
465465
{
466466
if (RB_UNLIKELY(search->has_matches)) {
@@ -479,17 +479,17 @@ static inline TARGET_SSE2 FORCE_INLINE unsigned char search_escape_basic_sse2(se
479479
}
480480
}
481481

482-
while (search->ptr + sizeof(__m128i) <= search->end) {
483-
int needs_escape_mask = sse2_update(search->ptr);
484-
485-
if (needs_escape_mask == 0) {
486-
search->ptr += sizeof(__m128i);
487-
continue;
488-
}
482+
SSE2StringScanIterator iterator = {
483+
.has_next_vector = has_next_vector,
484+
.ptr = ptr,
485+
.advance_by = advance_by,
486+
.set_match_mask = set_match_mask
487+
};
489488

489+
if (string_scan_simd_sse2(&iterator, search)) {
490490
search->has_matches = true;
491-
search->matches_mask = needs_escape_mask;
492491
search->chunk_base = search->ptr;
492+
search->chunk_end = search->ptr + sizeof(__m128i);
493493
return sse2_next_match(search);
494494
}
495495

@@ -498,7 +498,7 @@ static inline TARGET_SSE2 FORCE_INLINE unsigned char search_escape_basic_sse2(se
498498
if (remaining >= SIMD_MINIMUM_THRESHOLD) {
499499
char *s = copy_remaining_bytes(search, sizeof(__m128i), remaining);
500500

501-
int needs_escape_mask = sse2_update(s);
501+
int needs_escape_mask = compute_chunk_mask_sse2(s);
502502

503503
if (needs_escape_mask == 0) {
504504
// Nothing to escape, ensure search_flush doesn't do anything by setting

ext/json/ext/parser/parser.c

Lines changed: 51 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -846,7 +846,7 @@ static inline VALUE json_push_value(JSON_ParserState *state, JSON_ParserConfig *
846846
return value;
847847
}
848848

849-
static const bool string_scan[256] = {
849+
static const bool string_scan_table[256] = {
850850
// ASCII Control Characters
851851
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
852852
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
@@ -865,97 +865,76 @@ static const bool string_scan[256] = {
865865
#define FORCE_INLINE
866866
#endif
867867

868-
static inline bool FORCE_INLINE string_scan_basic(JSON_ParserState *state)
869-
{
870-
while (state->cursor < state->end) {
871-
if (RB_UNLIKELY(string_scan[(unsigned char)*state->cursor])) {
872-
return 1;
873-
}
874-
*state->cursor++;
875-
}
876-
return 0;
877-
}
878-
879-
static bool (*string_scan_impl)(JSON_ParserState *);
868+
// Not used at the moment.
869+
static SIMD_Implementation simd_impl = SIMD_NONE;
880870

881871
#ifdef HAVE_SIMD
882-
#ifdef HAVE_SIMD_NEON
883872

884-
static inline FORCE_INLINE bool string_scan_neon(JSON_ParserState *state)
873+
static inline FORCE_INLINE int has_next_vector(void *state, size_t width)
885874
{
886-
while (state->cursor + sizeof(uint8x16_t) <= state->end) {
887-
uint8x16_t chunk = vld1q_u8((const unsigned char *)state->cursor);
888-
889-
// Trick: c < 32 || c == 34 can be factored as c ^ 2 < 33
890-
// https://lemire.me/blog/2025/04/13/detect-control-characters-quotes-and-backslashes-efficiently-using-swar/
891-
const uint8x16_t too_low_or_dbl_quote = vcltq_u8(veorq_u8(chunk, vdupq_n_u8(2)), vdupq_n_u8(33));
892-
893-
uint8x16_t has_backslash = vceqq_u8(chunk, vdupq_n_u8('\\'));
894-
uint8x16_t needs_escape = vorrq_u8(too_low_or_dbl_quote, has_backslash);
895-
uint64_t mask = neon_match_mask(needs_escape);
896-
if (mask) {
897-
state->cursor += trailing_zeros64(mask) >> 2;
898-
return true;
899-
}
900-
901-
state->cursor += sizeof(uint8x16_t);
902-
}
903-
904-
if (state->cursor < state->end) {
905-
return string_scan_basic(state);
906-
}
907-
return 0;
875+
JSON_ParserState *s = state;
876+
return s->cursor + width <= s->end;
908877
}
909-
#endif /* HAVE_SIMD_NEON */
910-
911-
#ifdef HAVE_SIMD_SSE2
912878

913-
#define _mm_cmpge_epu8(a, b) _mm_cmpeq_epi8(_mm_max_epu8(a, b), a)
914-
#define _mm_cmple_epu8(a, b) _mm_cmpge_epu8(b, a)
915-
#define _mm_cmpgt_epu8(a, b) _mm_xor_si128(_mm_cmple_epu8(a, b), _mm_set1_epi8(-1))
916-
#define _mm_cmplt_epu8(a, b) _mm_cmpgt_epu8(b, a)
879+
static inline FORCE_INLINE const char *ptr(void *state) { return ((JSON_ParserState *) state)->cursor; }
880+
static inline FORCE_INLINE void advance_by(void *state, size_t count) { ((JSON_ParserState *) state)->cursor += count; }
917881

918-
#if defined(__clang__) || defined(__GNUC__)
919-
#define TARGET_SSE2 __attribute__((target("sse2")))
920-
#else
921-
#define TARGET_SSE2
922-
#endif
882+
#ifdef HAVE_SIMD_NEON
883+
static inline FORCE_INLINE void set_match_mask(void *state, uint64_t mask) { advance_by(state, trailing_zeros64(mask) >> 2); }
884+
#elif defined(HAVE_SIMD_SSE2)
885+
static inline FORCE_INLINE void set_match_mask(void *state, int mask) { advance_by(state, trailing_zeros(mask)); }
886+
#endif /* HAVE_SIMD_NEON */
887+
#endif /* HAVE_SIMD */
923888

924-
static inline TARGET_SSE2 FORCE_INLINE bool string_scan_sse2(JSON_ParserState *state)
889+
static inline bool FORCE_INLINE string_scan(JSON_ParserState *state)
925890
{
926-
while (state->cursor + sizeof(__m128i) <= state->end) {
927-
__m128i chunk = _mm_loadu_si128((__m128i const*)state->cursor);
928-
929-
// Trick: c < 32 || c == 34 can be factored as c ^ 2 < 33
930-
// https://lemire.me/blog/2025/04/13/detect-control-characters-quotes-and-backslashes-efficiently-using-swar/
931-
__m128i too_low_or_dbl_quote = _mm_cmplt_epu8(_mm_xor_si128(chunk, _mm_set1_epi8(2)), _mm_set1_epi8(33));
932-
__m128i has_backslash = _mm_cmpeq_epi8(chunk, _mm_set1_epi8('\\'));
933-
__m128i needs_escape = _mm_or_si128(too_low_or_dbl_quote, has_backslash);
934-
int mask = _mm_movemask_epi8(needs_escape);
935-
if (mask) {
936-
state->cursor += trailing_zeros(mask);
937-
return true;
938-
}
891+
#ifdef HAVE_SIMD
892+
#if defined(HAVE_SIMD_NEON)
893+
// Do not extract this structor to a global variable or make it static without profiling and
894+
// verifying that it does not cause performance regressions. clang 17 and gcc 14 currently
895+
// inline these methods if this structure is defined like this.
896+
NeonStringScanIterator iterator = {
897+
.has_next_vector = has_next_vector,
898+
.ptr = ptr,
899+
.advance_by = advance_by,
900+
.set_match_mask = set_match_mask
901+
};
939902

940-
state->cursor += sizeof(__m128i);
903+
if (string_scan_simd_neon(&iterator, state)) {
904+
return 1;
905+
}
906+
#elif defined(HAVE_SIMD_SSE2)
907+
if (simd_impl == SIMD_SSE2) {
908+
SSE2StringScanIterator iterator = {
909+
.has_next_vector = has_next_vector,
910+
.ptr = ptr,
911+
.advance_by = advance_by,
912+
.set_match_mask = set_match_mask
913+
};
914+
915+
if (string_scan_simd_sse2(&iterator, state)) {
916+
return 1;
917+
}
941918
}
919+
#endif /* HAVE_SIMD_NEON or HAVE_SIMD_SSE2 */
920+
#endif /* HAVE_SIMD */
942921

943-
if (state->cursor < state->end) {
944-
return string_scan_basic(state);
922+
while (state->cursor < state->end) {
923+
if (RB_UNLIKELY(string_scan_table[(unsigned char)*state->cursor])) {
924+
return 1;
925+
}
926+
*state->cursor++;
945927
}
946928
return 0;
947929
}
948-
#endif
949-
950-
#endif /* HAVE_SIMD */
951930

952931
static inline VALUE json_parse_string(JSON_ParserState *state, JSON_ParserConfig *config, bool is_name)
953932
{
954933
state->cursor++;
955934
const char *start = state->cursor;
956935
bool escaped = false;
957936

958-
while (RB_UNLIKELY(string_scan_impl(state))) {
937+
while (RB_UNLIKELY(string_scan(state))) {
959938
switch (*state->cursor) {
960939
case '"': {
961940
VALUE string = json_decode_string(state, config, start, state->cursor, escaped, is_name);
@@ -1504,21 +1483,7 @@ void Init_parser(void)
15041483
utf8_encindex = rb_utf8_encindex();
15051484
enc_utf8 = rb_utf8_encoding();
15061485

1507-
switch(find_simd_implementation()) {
15081486
#ifdef HAVE_SIMD
1509-
#ifdef HAVE_SIMD_NEON
1510-
case SIMD_NEON:
1511-
string_scan_impl = string_scan_neon;
1512-
break;
1513-
#endif /* HAVE_SIMD_NEON */
1514-
#ifdef HAVE_SIMD_SSE2
1515-
case SIMD_SSE2:
1516-
string_scan_impl = string_scan_sse2;
1517-
break;
1518-
#endif /* HAVE_SIMD_SSE2 */
1519-
#endif /* HAVE_SIMD */
1520-
default:
1521-
string_scan_impl = string_scan_basic;
1522-
break;
1523-
}
1487+
simd_impl = find_simd_implementation();
1488+
#endif
15241489
}

0 commit comments

Comments
 (0)