Skip to content

Commit 88972f9

Browse files
authored
Optimize oj_dump_cstr using SSE4.2 and SSSE3. (#973)
* Refactored and simplified the ARM Neon implementation. This optimizes the worst case synthetic benchmarks. * Move the definition of FORCE_INLINE out of the HAVE_SIMD_NEON block. * Formatting. * Renamed the expected real-world test data files. * Initial optimization of oj_dump_cstr using SSE4.2 on x86-64 platforms. * Refactor the code to reduce duplication. * SSE42 bugfixes. * Formatting. * Added SIMD_MINIMUM_THRESHOLD after accidentally removing it when merging the develop branch. * More fixes after merging develop into this branch. * clang-format * Refactor oj_dump_cstr to use runtime SIMD detection of SSE4. * Wrap SSE4.2 initialization code in an ifdef. * formatting
1 parent 5e0260c commit 88972f9

3 files changed

Lines changed: 194 additions & 9 deletions

File tree

ext/oj/dump.c

Lines changed: 148 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,45 @@ void initialize_neon(void) {
201201
}
202202
#endif
203203

204+
#ifdef HAVE_SIMD_SSE4_2
205+
206+
static __m128i hibit_friendly_chars_sse42[8];
207+
208+
// From: https://stackoverflow.com/questions/36998538/fastest-way-to-horizontally-sum-sse-unsigned-byte-vector
209+
inline uint32_t _mm_sum_epu8(const __m128i v) {
210+
__m128i vsum = _mm_sad_epu8(v, _mm_setzero_si128());
211+
return _mm_cvtsi128_si32(vsum) + _mm_extract_epi16(vsum, 4);
212+
}
213+
214+
inline static OJ_TARGET_SSE42 size_t hibit_friendly_size_sse42(const uint8_t *str, size_t len) {
215+
size_t size = 0;
216+
size_t i = 0;
217+
218+
for (; i + sizeof(__m128i) <= len; i += sizeof(__m128i), str += sizeof(__m128i)) {
219+
size += sizeof(__m128i);
220+
221+
__m128i chunk = _mm_loadu_si128((__m128i *)str);
222+
__m128i tmp = vector_lookup_sse42(chunk, hibit_friendly_chars_sse42, 8);
223+
size += _mm_sum_epu8(tmp);
224+
}
225+
size_t total = size + calculate_string_size(str, len - i, hibit_friendly_chars);
226+
return total;
227+
}
228+
229+
void OJ_TARGET_SSE42 initialize_sse42(void) {
230+
for (int i = 0; i < 8; i++) {
231+
hibit_friendly_chars_sse42[i] = _mm_sub_epi8(
232+
_mm_loadu_si128((__m128i *)(hibit_friendly_chars + i * sizeof(__m128i))),
233+
_mm_set1_epi8('1'));
234+
}
235+
}
236+
237+
#else
238+
239+
#define SIMD_TARGET
240+
241+
#endif /* HAVE_SIMD_SSE4_2 */
242+
204243
inline static size_t hibit_friendly_size(const uint8_t *str, size_t len) {
205244
#ifdef HAVE_SIMD_NEON
206245
size_t size = 0;
@@ -220,6 +259,13 @@ inline static size_t hibit_friendly_size(const uint8_t *str, size_t len) {
220259

221260
size_t total = size + calculate_string_size(str, len - i, hibit_friendly_chars);
222261
return total;
262+
#elif defined(HAVE_SIMD_SSE4_2)
263+
if (SIMD_Impl == SIMD_SSE42) {
264+
if (len >= sizeof(__m128i)) {
265+
return hibit_friendly_size_sse42(str, len);
266+
}
267+
}
268+
return calculate_string_size(str, len, hibit_friendly_chars);
223269
#else
224270
return calculate_string_size(str, len, hibit_friendly_chars);
225271
#endif
@@ -944,6 +990,34 @@ neon_update(const char *str, uint8x16x4_t *cmap_neon, int neon_table_size, bool
944990
return result;
945991
}
946992

993+
#elif defined(HAVE_SIMD_SSE4_2)
994+
typedef struct _sse42_match_result {
995+
__m128i actions;
996+
bool needs_escape;
997+
int escape_mask;
998+
bool has_some_hibit;
999+
bool do_unicode_validation;
1000+
} sse42_match_result;
1001+
1002+
static inline OJ_TARGET_SSE42 sse42_match_result
1003+
sse42_update(const char *str, __m128i *cmap_sse42, int sse42_tab_size, bool do_unicode_validation, bool has_hi) {
1004+
sse42_match_result result = {.has_some_hibit = false, .do_unicode_validation = false};
1005+
1006+
__m128i chunk = _mm_loadu_si128((__m128i *)str);
1007+
__m128i actions = vector_lookup_sse42(chunk, cmap_sse42, sse42_tab_size);
1008+
__m128i needs_escape = _mm_xor_si128(_mm_cmpeq_epi8(actions, _mm_setzero_si128()), _mm_set1_epi8(0xFF));
1009+
result.actions = _mm_add_epi8(actions, _mm_set1_epi8('1'));
1010+
1011+
result.escape_mask = _mm_movemask_epi8(needs_escape);
1012+
result.needs_escape = result.escape_mask != 0;
1013+
if (has_hi && do_unicode_validation) {
1014+
__m128i has_some_hibit = _mm_and_si128(chunk, _mm_set1_epi8(0x80));
1015+
result.has_some_hibit = _mm_movemask_epi8(has_some_hibit) != 0;
1016+
result.do_unicode_validation = has_hi && do_unicode_validation && result.has_some_hibit;
1017+
}
1018+
return result;
1019+
}
1020+
9471021
#endif /* HAVE_SIMD_NEON */
9481022

9491023
static inline FORCE_INLINE const char *process_character(char action,
@@ -1023,6 +1097,9 @@ void oj_dump_cstr(const char *str, size_t cnt, bool is_sym, bool escape1, Out ou
10231097
#ifdef HAVE_SIMD_NEON
10241098
uint8x16x4_t *cmap_neon = NULL;
10251099
int neon_table_size = 0;
1100+
#elif defined(HAVE_SIMD_SSE4_2)
1101+
__m128i *cmap_sse42 = NULL;
1102+
int sse42_tab_size;
10261103
#endif /* HAVE_SIMD_NEON */
10271104
const char *orig = str;
10281105
bool has_hi = false;
@@ -1091,6 +1168,9 @@ void oj_dump_cstr(const char *str, size_t cnt, bool is_sym, bool escape1, Out ou
10911168
#ifdef HAVE_SIMD_NEON
10921169
cmap_neon = hibit_friendly_chars_neon;
10931170
neon_table_size = 2;
1171+
#elif defined(HAVE_SIMD_SSE4_2)
1172+
cmap_sse42 = hibit_friendly_chars_sse42;
1173+
sse42_tab_size = 8;
10941174
#endif /* HAVE_NEON_SIMD */
10951175
size = hibit_friendly_size((uint8_t *)str, cnt);
10961176
}
@@ -1118,21 +1198,32 @@ void oj_dump_cstr(const char *str, size_t cnt, bool is_sym, bool escape1, Out ou
11181198
if (is_sym) {
11191199
*out->cur++ = ':';
11201200
}
1121-
#ifdef HAVE_SIMD_NEON
1122-
const char *chunk_start;
1123-
const char *chunk_end;
1124-
const char *cursor = str;
1125-
bool use_neon = (cmap_neon != NULL && cnt >= (sizeof(uint8x16_t))) ? true : false;
1126-
char matches[16];
1201+
1202+
#if defined(HAVE_SIMD_NEON) || defined(HAVE_SIMD_SSE4_2)
1203+
11271204
#define SEARCH_FLUSH \
11281205
if (str > cursor) { \
11291206
APPEND_CHARS(out->cur, cursor, str - cursor); \
11301207
cursor = str; \
11311208
}
11321209

1133-
#endif /* HAVE_SIMD_NEON */
1210+
const char *chunk_start;
1211+
const char *chunk_end;
1212+
const char *cursor = str;
1213+
char matches[16];
1214+
#endif /* HAVE_SIMD_NEON || HAVE_SIMD_SSE4_2 */
1215+
1216+
#if defined(HAVE_SIMD_NEON)
1217+
bool use_simd = (cmap_neon != NULL && cnt >= (sizeof(uint8x16_t))) ? true : false;
1218+
#elif defined(HAVE_SIMD_SSE4_2)
1219+
bool use_simd = false;
1220+
if (SIMD_Impl == SIMD_SSE42) {
1221+
use_simd = (cmap_sse42 != NULL && cnt >= (sizeof(__m128i))) ? true : false;
1222+
}
1223+
#endif
1224+
11341225
#ifdef HAVE_SIMD_NEON
1135-
if (use_neon) {
1226+
if (use_simd) {
11361227
while (str < end) {
11371228
const char *chunk_ptr = NULL;
11381229
if (str + sizeof(uint8x16_t) <= end) {
@@ -1195,7 +1286,55 @@ void oj_dump_cstr(const char *str, size_t cnt, bool is_sym, bool escape1, Out ou
11951286
}
11961287
SEARCH_FLUSH;
11971288
}
1198-
#endif /* HAVE_SIMD_NEON */
1289+
#endif
1290+
1291+
#ifdef HAVE_SIMD_SSE4_2
1292+
if (SIMD_Impl == SIMD_SSE42) {
1293+
if (use_simd) {
1294+
while (str < end) {
1295+
const char *chunk_ptr = NULL;
1296+
if (str + sizeof(__m128i) <= end) {
1297+
chunk_ptr = str;
1298+
chunk_start = str;
1299+
chunk_end = str + sizeof(__m128i);
1300+
} else if ((end - str) >= SIMD_MINIMUM_THRESHOLD) {
1301+
memset(out->cur, 'A', sizeof(__m128i));
1302+
memcpy(out->cur, str, (end - str));
1303+
chunk_ptr = out->cur;
1304+
chunk_start = str;
1305+
chunk_end = end;
1306+
} else {
1307+
break;
1308+
}
1309+
sse42_match_result result = sse42_update(chunk_ptr,
1310+
cmap_sse42,
1311+
sse42_tab_size,
1312+
do_unicode_validation,
1313+
has_hi);
1314+
if ((result.do_unicode_validation) || result.needs_escape) {
1315+
SEARCH_FLUSH;
1316+
_mm_storeu_si128((__m128i *)matches, result.actions);
1317+
while (str < chunk_end) {
1318+
long match_index = str - chunk_start;
1319+
str = process_character(matches[match_index],
1320+
str,
1321+
end,
1322+
out,
1323+
orig,
1324+
do_unicode_validation,
1325+
&check_start);
1326+
str++;
1327+
}
1328+
cursor = str;
1329+
continue;
1330+
}
1331+
str = chunk_end;
1332+
}
1333+
SEARCH_FLUSH;
1334+
}
1335+
}
1336+
#endif /* HAVE_SIMD_SSE4_2 */
1337+
11991338
for (; str < end; str++) {
12001339
str = process_character(cmap[(uint8_t)*str], str, end, out, orig, do_unicode_validation, &check_start);
12011340
}

ext/oj/oj.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,8 @@ pthread_mutex_t oj_cache_mutex;
167167
VALUE oj_cache_mutex = Qnil;
168168
#endif
169169

170+
SIMD_Implementation SIMD_Impl = SIMD_NONE;
171+
170172
extern void oj_parser_init();
171173

172174
const char oj_json_class[] = "json_class";
@@ -2152,10 +2154,18 @@ void Init_oj(void) {
21522154
#endif
21532155
oj_init_doc();
21542156

2157+
SIMD_Impl = oj_get_simd_implementation();
2158+
21552159
oj_parser_init();
21562160
oj_scanner_init();
21572161

21582162
#ifdef HAVE_SIMD_NEON
21592163
initialize_neon();
21602164
#endif /* HAVE_SIMD_NEON */
2165+
2166+
#ifdef HAVE_SIMD_SSE4_2
2167+
if (SIMD_Impl == SIMD_SSE42) {
2168+
initialize_sse42();
2169+
}
2170+
#endif /* HAVE_SIMD_SSE4_2 */
21612171
}

ext/oj/simd.h

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
// SIMD implementation enum - used for runtime selection
99
typedef enum _simd_implementation { SIMD_NONE, SIMD_NEON, SIMD_SSE2, SIMD_SSE42 } SIMD_Implementation;
1010

11+
// Define in oj.c.
12+
extern SIMD_Implementation SIMD_Impl;
13+
1114
// Runtime CPU detection function (implemented in oj.c)
1215
SIMD_Implementation oj_get_simd_implementation(void);
1316

@@ -133,4 +136,37 @@ static inline int oj_ctz_fallback(unsigned int x) {
133136
#define SIMD_TYPE "none"
134137
#endif
135138

139+
#if defined(HAVE_SIMD_SSE4_2)
140+
141+
#define SIMD_MINIMUM_THRESHOLD 6
142+
143+
extern void initialize_sse42(void);
144+
145+
static inline OJ_TARGET_SSE42 __m128i vector_lookup_sse42(__m128i input, __m128i *lookup_table, int tab_size) {
146+
// Extract high 4 bits to determine which 16-byte chunk (0-15)
147+
__m128i hi_index = _mm_and_si128(_mm_srli_epi32(input, 4), _mm_set1_epi8(0x0F));
148+
149+
// Extract low 4 bits for index within the chunk (0-15)
150+
__m128i low_index = _mm_and_si128(input, _mm_set1_epi8(0x0F));
151+
152+
// Perform lookups in all 16 tables
153+
__m128i results[16];
154+
for (int i = 0; i < tab_size; i++) {
155+
results[i] = _mm_shuffle_epi8(lookup_table[i], low_index);
156+
}
157+
158+
// Create masks for each chunk and blend results
159+
__m128i final_result = _mm_setzero_si128();
160+
161+
for (int i = 0; i < tab_size; i++) {
162+
__m128i mask = _mm_cmpeq_epi8(hi_index, _mm_set1_epi8(i));
163+
__m128i masked_result = _mm_and_si128(mask, results[i]);
164+
final_result = _mm_or_si128(final_result, masked_result);
165+
}
166+
167+
return final_result;
168+
}
169+
170+
#endif
171+
136172
#endif /* OJ_SIMD_H */

0 commit comments

Comments
 (0)