Skip to content

Commit 840990c

Browse files
committed
Merge branch 'develop' into optimize-oj_dump_cstr-sse4
2 parents 37dc864 + 7355146 commit 840990c

13 files changed

Lines changed: 233 additions & 72 deletions

File tree

.github/workflows/CI.yml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
matrix:
1717
# Due to https://github.com/actions/runner/issues/849, we have to use quotes for '3.0'
1818
ruby:
19-
- head
19+
# - head
2020
- '3.4'
2121
- '3.3'
2222
- '3.2'
@@ -55,9 +55,10 @@ jobs:
5555
- ruby: 'truffleruby'
5656
gemfile: no_rails
5757
os: ubuntu
58-
- ruby: 'ucrt'
59-
gemfile: no_rails
60-
os: windows
58+
# no longer supported or until min stack size issue can be resolved
59+
# - ruby: 'ucrt'
60+
# gemfile: no_rails
61+
# os: windows
6162

6263
env:
6364
BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile
@@ -66,7 +67,7 @@ jobs:
6667
continue-on-error: ${{ matrix.ruby == 'head'}}
6768
name: Ruby ${{ matrix.ruby }} (${{ matrix.os }}${{ matrix.gemfile != 'no_rails' && ', ' || '' }}${{ matrix.gemfile != 'no_rails' && matrix.gemfile || '' }})
6869
steps:
69-
- uses: actions/checkout@v4
70+
- uses: actions/checkout@v6
7071
- uses: ruby/setup-ruby@v1
7172
with:
7273
ruby-version: ${{ matrix.ruby }}

.github/workflows/clang-format.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ jobs:
2424
runs-on: ubuntu-latest
2525

2626
steps:
27-
- uses: actions/checkout@v4
27+
- uses: actions/checkout@v6
2828

2929
- name: Run clang-format style check
3030
uses: jidicula/clang-format-action@v4.15.0

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# CHANGELOG
22

3+
## 3.16.13 - 2025-12-05
4+
5+
- Fixed rails encoding for Hash and Array subclasses.
6+
7+
## 3.16.12 - 2025-10-29
8+
9+
- Fixed dump realloc bug that occurred when using the compat mode dump options.
10+
311
## 3.16.11 - 2025-05-29
412

513
- Fixed range encoding with the :circular option

ext/oj/dump.c

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

204-
#ifdef OJ_USE_SSE4_2
204+
#ifdef HAVE_SIMD_SSE4_2
205205

206206
static size_t (*hibit_friendly_size_simd)(const uint8_t *str, size_t len) = NULL;
207207
static __m128i hibit_friendly_chars_sse42[8];
@@ -995,7 +995,7 @@ neon_update(const char *str, uint8x16x4_t *cmap_neon, int neon_table_size, bool
995995
return result;
996996
}
997997

998-
#elif defined(OJ_USE_SSE4_2)
998+
#elif defined(HAVE_SIMD_SSE4_2)
999999
typedef struct _sse42_match_result {
10001000
__m128i actions;
10011001
bool needs_escape;
@@ -1100,11 +1100,11 @@ void oj_dump_cstr(const char *str, size_t cnt, bool is_sym, bool escape1, Out ou
11001100
size_t size;
11011101
char *cmap;
11021102
#ifdef HAVE_SIMD_NEON
1103-
uint8x16x4_t *cmap_neon = NULL;
1104-
int neon_table_size;
1103+
uint8x16x4_t *cmap_neon = NULL;
1104+
int neon_table_size = 0;
11051105
#elif defined(OJ_USE_SSE4_2)
1106-
__m128i *cmap_sse42 = NULL;
1107-
int sse42_tab_size;
1106+
__m128i *cmap_sse42 = NULL;
1107+
int sse42_tab_size;
11081108
#endif /* HAVE_SIMD_NEON */
11091109
const char *orig = str;
11101110
bool has_hi = false;
@@ -1173,7 +1173,7 @@ void oj_dump_cstr(const char *str, size_t cnt, bool is_sym, bool escape1, Out ou
11731173
#ifdef HAVE_SIMD_NEON
11741174
cmap_neon = hibit_friendly_chars_neon;
11751175
neon_table_size = 2;
1176-
#elif defined(OJ_USE_SSE4_2)
1176+
#elif defined(HAVE_SIMD_SSE4_2)
11771177
cmap_sse42 = hibit_friendly_chars_sse42;
11781178
sse42_tab_size = 8;
11791179
#endif /* HAVE_NEON_SIMD */
@@ -1204,7 +1204,7 @@ void oj_dump_cstr(const char *str, size_t cnt, bool is_sym, bool escape1, Out ou
12041204
*out->cur++ = ':';
12051205
}
12061206

1207-
#if defined(HAVE_SIMD_NEON) || defined(OJ_USE_SSE4_2)
1207+
#if defined(HAVE_SIMD_NEON) || defined(HAVE_SIMD_SSE4_2)
12081208

12091209
#define SEARCH_FLUSH \
12101210
if (str > cursor) { \
@@ -1216,11 +1216,11 @@ void oj_dump_cstr(const char *str, size_t cnt, bool is_sym, bool escape1, Out ou
12161216
const char *chunk_end;
12171217
const char *cursor = str;
12181218
char matches[16];
1219-
#endif /* HAVE_SIMD_NEON || OJ_USE_SSE4_2 */
1219+
#endif /* HAVE_SIMD_NEON || HAVE_SIMD_SSE4_2 */
12201220

12211221
#if defined(HAVE_SIMD_NEON)
12221222
bool use_simd = (cmap_neon != NULL && cnt >= (sizeof(uint8x16_t))) ? true : false;
1223-
#elif defined(OJ_USE_SSE4_2)
1223+
#elif defined(HAVE_SIMD_SSE4_2)
12241224
bool use_simd = (cmap_sse42 != NULL && cnt >= (sizeof(__m128i))) ? true : false;
12251225
#endif
12261226

@@ -1290,7 +1290,7 @@ void oj_dump_cstr(const char *str, size_t cnt, bool is_sym, bool escape1, Out ou
12901290
}
12911291
#endif
12921292

1293-
#ifdef OJ_USE_SSE4_2
1293+
#ifdef HAVE_SIMD_SSE4_2
12941294
if (use_simd) {
12951295
while (str < end) {
12961296
const char *chunk_ptr = NULL;
@@ -1333,7 +1333,7 @@ void oj_dump_cstr(const char *str, size_t cnt, bool is_sym, bool escape1, Out ou
13331333
}
13341334
SEARCH_FLUSH;
13351335
}
1336-
#endif /* OJ_USE_SSE4_2 */
1336+
#endif /* HAVE_SIMD_SSE4_2 */
13371337

13381338
for (; str < end; str++) {
13391339
str = process_character(cmap[(uint8_t)*str], str, end, out, orig, do_unicode_validation, &check_start);

ext/oj/dump_compat.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,10 +148,10 @@ static void dump_array(VALUE a, int depth, Out out, bool as_ok) {
148148
} else {
149149
size = d2 * out->indent + 2;
150150
}
151-
assure_size(out, size * cnt);
152151
cnt--;
153152
for (i = 0; i <= cnt; i++) {
154153
if (out->opts->dump_opts.use) {
154+
assure_size(out, size);
155155
if (0 < out->opts->dump_opts.array_size) {
156156
APPEND_CHARS(out->cur, out->opts->dump_opts.array_nl, out->opts->dump_opts.array_size);
157157
}

ext/oj/extconf.rb

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,12 @@
3535

3636
dflags['OJ_DEBUG'] = true unless ENV['OJ_DEBUG'].nil?
3737

38-
if with_config('--with-sse42')
39-
if try_cflags('-msse4.2')
40-
$CPPFLAGS += ' -msse4.2'
41-
dflags['OJ_USE_SSE4_2'] = 1
42-
else
43-
warn 'SSE 4.2 is not supported on this platform.'
44-
end
38+
# Enable SIMD optimizations - try SSE4.2 on x86_64 for best performance
39+
# Falls back to SSE2 or compiler defaults if not available
40+
if try_cflags('-msse4.2')
41+
$CPPFLAGS += ' -msse4.2'
42+
elsif try_cflags('-msse2')
43+
$CPPFLAGS += ' -msse2'
4544
end
4645

4746
if enable_config('trace-log', false)

ext/oj/parse.c

Lines changed: 136 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,9 @@
1515
#include "mem.h"
1616
#include "oj.h"
1717
#include "rxclass.h"
18+
#include "simd.h"
1819
#include "val_stack.h"
1920

20-
#ifdef OJ_USE_SSE4_2
21-
#include <nmmintrin.h>
22-
#endif
23-
2421
// Workaround in case INFINITY is not defined in math.h or if the OS is CentOS
2522
#define OJ_INFINITY (1.0 / 0.0)
2623

@@ -202,23 +199,143 @@ static inline const char *scan_string_noSIMD(const char *str, const char *end) {
202199
return str;
203200
}
204201

205-
#ifdef OJ_USE_SSE4_2
206-
static inline const char *scan_string_SIMD(const char *str, const char *end) {
207-
static const char chars[16] = "\x00\\\"";
208-
const __m128i terminate = _mm_loadu_si128((const __m128i *)&chars[0]);
209-
const char *_end = (const char *)(end - 16);
202+
#ifdef HAVE_SIMD_SSE4_2
203+
// Optimized SIMD string scanner using SSE4.2 instructions
204+
// Uses prefetching and processes multiple chunks in parallel to reduce latency
205+
static inline const char *scan_string_SSE42(const char *str, const char *end) {
206+
static const char chars[16] = "\x00\\\"";
207+
const __m128i terminate = _mm_loadu_si128((const __m128i *)&chars[0]);
208+
const char *safe_end_64 = end - 64;
209+
const char *safe_end_16 = end - 16;
210+
211+
// Process 64 bytes at a time with parallel SIMD operations
212+
// This reduces pipeline stalls and improves instruction-level parallelism
213+
while (str <= safe_end_64) {
214+
// Prefetch next cache line for better memory throughput
215+
__builtin_prefetch(str + 64, 0, 0);
216+
217+
// Load and compare 4 chunks in parallel
218+
const __m128i chunk0 = _mm_loadu_si128((const __m128i *)(str));
219+
const __m128i chunk1 = _mm_loadu_si128((const __m128i *)(str + 16));
220+
const __m128i chunk2 = _mm_loadu_si128((const __m128i *)(str + 32));
221+
const __m128i chunk3 = _mm_loadu_si128((const __m128i *)(str + 48));
222+
223+
const int r0 = _mm_cmpestri(terminate,
224+
3,
225+
chunk0,
226+
16,
227+
_SIDD_UBYTE_OPS | _SIDD_CMP_EQUAL_ANY | _SIDD_LEAST_SIGNIFICANT);
228+
if (__builtin_expect(r0 != 16, 0))
229+
return str + r0;
230+
231+
const int r1 = _mm_cmpestri(terminate,
232+
3,
233+
chunk1,
234+
16,
235+
_SIDD_UBYTE_OPS | _SIDD_CMP_EQUAL_ANY | _SIDD_LEAST_SIGNIFICANT);
236+
if (__builtin_expect(r1 != 16, 0))
237+
return str + 16 + r1;
238+
239+
const int r2 = _mm_cmpestri(terminate,
240+
3,
241+
chunk2,
242+
16,
243+
_SIDD_UBYTE_OPS | _SIDD_CMP_EQUAL_ANY | _SIDD_LEAST_SIGNIFICANT);
244+
if (__builtin_expect(r2 != 16, 0))
245+
return str + 32 + r2;
246+
247+
const int r3 = _mm_cmpestri(terminate,
248+
3,
249+
chunk3,
250+
16,
251+
_SIDD_UBYTE_OPS | _SIDD_CMP_EQUAL_ANY | _SIDD_LEAST_SIGNIFICANT);
252+
if (__builtin_expect(r3 != 16, 0))
253+
return str + 48 + r3;
254+
255+
str += 64;
256+
}
210257

211-
for (; str <= _end; str += 16) {
258+
// Handle remaining 16-byte chunks
259+
for (; str <= safe_end_16; str += 16) {
212260
const __m128i string = _mm_loadu_si128((const __m128i *)str);
213261
const int r = _mm_cmpestri(terminate,
214262
3,
215263
string,
216264
16,
217265
_SIDD_UBYTE_OPS | _SIDD_CMP_EQUAL_ANY | _SIDD_LEAST_SIGNIFICANT);
218-
if (r != 16) {
219-
str = (char *)(str + r);
220-
return str;
221-
}
266+
if (r != 16)
267+
return str + r;
268+
}
269+
270+
return scan_string_noSIMD(str, end);
271+
}
272+
#endif
273+
274+
#ifdef HAVE_SIMD_SSE2
275+
// Optimized SSE2 string scanner (fallback for older x86_64 CPUs)
276+
// Uses SSE2 instructions with prefetching and parallel processing
277+
static inline const char *scan_string_SSE2(const char *str, const char *end) {
278+
const char *safe_end_64 = end - 64;
279+
const char *safe_end_16 = end - 16;
280+
281+
// Create comparison vectors for our three special characters
282+
const __m128i null_char = _mm_setzero_si128();
283+
const __m128i backslash = _mm_set1_epi8('\\');
284+
const __m128i quote = _mm_set1_epi8('"');
285+
286+
// Process 64 bytes at a time for better throughput
287+
while (str <= safe_end_64) {
288+
__builtin_prefetch(str + 64, 0, 0);
289+
290+
// Load 4 chunks
291+
const __m128i chunk0 = _mm_loadu_si128((const __m128i *)(str));
292+
const __m128i chunk1 = _mm_loadu_si128((const __m128i *)(str + 16));
293+
const __m128i chunk2 = _mm_loadu_si128((const __m128i *)(str + 32));
294+
const __m128i chunk3 = _mm_loadu_si128((const __m128i *)(str + 48));
295+
296+
// Compare all chunks (allows CPU to parallelize)
297+
const __m128i cmp0 = _mm_or_si128(
298+
_mm_or_si128(_mm_cmpeq_epi8(chunk0, null_char), _mm_cmpeq_epi8(chunk0, backslash)),
299+
_mm_cmpeq_epi8(chunk0, quote));
300+
const __m128i cmp1 = _mm_or_si128(
301+
_mm_or_si128(_mm_cmpeq_epi8(chunk1, null_char), _mm_cmpeq_epi8(chunk1, backslash)),
302+
_mm_cmpeq_epi8(chunk1, quote));
303+
const __m128i cmp2 = _mm_or_si128(
304+
_mm_or_si128(_mm_cmpeq_epi8(chunk2, null_char), _mm_cmpeq_epi8(chunk2, backslash)),
305+
_mm_cmpeq_epi8(chunk2, quote));
306+
const __m128i cmp3 = _mm_or_si128(
307+
_mm_or_si128(_mm_cmpeq_epi8(chunk3, null_char), _mm_cmpeq_epi8(chunk3, backslash)),
308+
_mm_cmpeq_epi8(chunk3, quote));
309+
310+
// Convert to masks
311+
int mask0 = _mm_movemask_epi8(cmp0);
312+
if (__builtin_expect(mask0 != 0, 0))
313+
return str + __builtin_ctz(mask0);
314+
315+
int mask1 = _mm_movemask_epi8(cmp1);
316+
if (__builtin_expect(mask1 != 0, 0))
317+
return str + 16 + __builtin_ctz(mask1);
318+
319+
int mask2 = _mm_movemask_epi8(cmp2);
320+
if (__builtin_expect(mask2 != 0, 0))
321+
return str + 32 + __builtin_ctz(mask2);
322+
323+
int mask3 = _mm_movemask_epi8(cmp3);
324+
if (__builtin_expect(mask3 != 0, 0))
325+
return str + 48 + __builtin_ctz(mask3);
326+
327+
str += 64;
328+
}
329+
330+
// Handle remaining 16-byte chunks
331+
for (; str <= safe_end_16; str += 16) {
332+
const __m128i chunk = _mm_loadu_si128((const __m128i *)str);
333+
const __m128i matches = _mm_or_si128(
334+
_mm_or_si128(_mm_cmpeq_epi8(chunk, null_char), _mm_cmpeq_epi8(chunk, backslash)),
335+
_mm_cmpeq_epi8(chunk, quote));
336+
int mask = _mm_movemask_epi8(matches);
337+
if (mask != 0)
338+
return str + __builtin_ctz(mask);
222339
}
223340

224341
return scan_string_noSIMD(str, end);
@@ -228,9 +345,12 @@ static inline const char *scan_string_SIMD(const char *str, const char *end) {
228345
static const char *(*scan_func)(const char *str, const char *end) = scan_string_noSIMD;
229346

230347
void oj_scanner_init(void) {
231-
#ifdef OJ_USE_SSE4_2
232-
scan_func = scan_string_SIMD;
348+
#ifdef HAVE_SIMD_SSE4_2
349+
scan_func = scan_string_SSE42;
350+
#elif defined(HAVE_SIMD_SSE2)
351+
scan_func = scan_string_SSE2;
233352
#endif
353+
// Note: ARM NEON string scanning would be added here if needed
234354
}
235355

236356
// entered at /

ext/oj/rails.c

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -661,13 +661,15 @@ static VALUE encoder_new(int argc, VALUE *argv, VALUE self) {
661661
Encoder e = OJ_R_ALLOC(struct _encoder);
662662

663663
e->opts = oj_default_options;
664-
e->arg = Qnil;
665664
copy_opts(&ropts, &e->ropts);
666665

667666
if (1 <= argc && Qnil != *argv) {
668-
oj_parse_options(*argv, &e->opts);
669667
e->arg = *argv;
668+
} else {
669+
e->arg = rb_hash_new();
670670
}
671+
oj_parse_options(e->arg, &e->opts);
672+
671673
return TypedData_Wrap_Struct(encoder_class, &oj_encoder_type, e);
672674
}
673675

0 commit comments

Comments
 (0)