From e1cf787f43a3268bf9cc1450ada03b652d661894 Mon Sep 17 00:00:00 2001 From: Yusuke Endoh Date: Wed, 27 Dec 2023 15:45:09 +0900 Subject: [PATCH 01/10] Stop prebuilding array_delim The purpose of this change is to exploit `fbuffer_append_char` that is faster than `fbuffer_append`. `array_delim` was a buffer that concatenated a single comma with `array_nl`. However, in the typical use case (`JSON.generate(data)`), `array_nl` is empty. This means that `array_delim` was a single-character buffer in many cases. `fbuffer_append(buffer, array_delim)` used `memcpy` to copy one byte, which was not so efficient. Rather, this change uses `fbuffer_append_char(buffer, ',')` and then `fbuffer_append(buffer, array_nl)` only when `array_nl` is not NULL. This speeds up `JSON.generate` by about 9% in a benchmark. --- ext/json/ext/generator/generator.c | 17 ++++------------- ext/json/ext/generator/generator.h | 1 - 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/ext/json/ext/generator/generator.c b/ext/json/ext/generator/generator.c index a33df848d..2994795d9 100644 --- a/ext/json/ext/generator/generator.c +++ b/ext/json/ext/generator/generator.c @@ -610,7 +610,6 @@ static void State_free(void *ptr) if (state->space_before) ruby_xfree(state->space_before); if (state->object_nl) ruby_xfree(state->object_nl); if (state->array_nl) ruby_xfree(state->array_nl); - if (state->array_delim) fbuffer_free(state->array_delim); if (state->object_delim) fbuffer_free(state->object_delim); if (state->object_delim2) fbuffer_free(state->object_delim2); ruby_xfree(state); @@ -625,7 +624,6 @@ static size_t State_memsize(const void *ptr) if (state->space_before) size += state->space_before_len + 1; if (state->object_nl) size += state->object_nl_len + 1; if (state->array_nl) size += state->array_nl_len + 1; - if (state->array_delim) size += FBUFFER_CAPA(state->array_delim); if (state->object_delim) size += FBUFFER_CAPA(state->object_delim); if (state->object_delim2) size += FBUFFER_CAPA(state->object_delim2); return size; @@ -922,8 +920,6 @@ static void generate_json_array(FBuffer *buffer, VALUE Vstate, JSON_Generator_St char *indent = state->indent; long indent_len = state->indent_len; long max_nesting = state->max_nesting; - char *delim = FBUFFER_PTR(state->array_delim); - long delim_len = FBUFFER_LEN(state->array_delim); long depth = ++state->depth; int i, j; if (max_nesting != 0 && depth > max_nesting) { @@ -933,7 +929,10 @@ static void generate_json_array(FBuffer *buffer, VALUE Vstate, JSON_Generator_St fbuffer_append_char(buffer, '['); if (array_nl) fbuffer_append(buffer, array_nl, array_nl_len); for(i = 0; i < RARRAY_LEN(obj); i++) { - if (i > 0) fbuffer_append(buffer, delim, delim_len); + if (i > 0) { + fbuffer_append_char(buffer, ','); + if (RB_UNLIKELY(state->array_nl)) fbuffer_append(buffer, state->array_nl, state->array_nl_len); + } if (indent) { for (j = 0; j < depth; j++) { fbuffer_append(buffer, indent, indent_len); @@ -1086,13 +1085,6 @@ static FBuffer *cState_prepare_buffer(VALUE self) fbuffer_append_char(state->object_delim2, ':'); if (state->space) fbuffer_append(state->object_delim2, state->space, state->space_len); - if (state->array_delim) { - fbuffer_clear(state->array_delim); - } else { - state->array_delim = fbuffer_alloc(16); - } - fbuffer_append_char(state->array_delim, ','); - if (state->array_nl) fbuffer_append(state->array_delim, state->array_nl, state->array_nl_len); return buffer; } @@ -1171,7 +1163,6 @@ static VALUE cState_init_copy(VALUE obj, VALUE orig) objState->space_before = fstrndup(origState->space_before, origState->space_before_len); objState->object_nl = fstrndup(origState->object_nl, origState->object_nl_len); objState->array_nl = fstrndup(origState->array_nl, origState->array_nl_len); - if (origState->array_delim) objState->array_delim = fbuffer_dup(origState->array_delim); if (origState->object_delim) objState->object_delim = fbuffer_dup(origState->object_delim); if (origState->object_delim2) objState->object_delim2 = fbuffer_dup(origState->object_delim2); return obj; diff --git a/ext/json/ext/generator/generator.h b/ext/json/ext/generator/generator.h index 1a736b84d..ff93f210c 100644 --- a/ext/json/ext/generator/generator.h +++ b/ext/json/ext/generator/generator.h @@ -66,7 +66,6 @@ typedef struct JSON_Generator_StateStruct { long object_nl_len; char *array_nl; long array_nl_len; - FBuffer *array_delim; FBuffer *object_delim; FBuffer *object_delim2; long max_nesting; From 08f79bb4f16e5dcfe059da0de45759d2904c363f Mon Sep 17 00:00:00 2001 From: Yusuke Endoh Date: Wed, 27 Dec 2023 15:48:17 +0900 Subject: [PATCH 02/10] Stop prebuilding object_delim This speeds up `JSON.generate` by about 4% in a benchmark --- ext/json/ext/generator/generator.c | 13 +------------ ext/json/ext/generator/generator.h | 1 - 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/ext/json/ext/generator/generator.c b/ext/json/ext/generator/generator.c index 2994795d9..a25e77780 100644 --- a/ext/json/ext/generator/generator.c +++ b/ext/json/ext/generator/generator.c @@ -610,7 +610,6 @@ static void State_free(void *ptr) if (state->space_before) ruby_xfree(state->space_before); if (state->object_nl) ruby_xfree(state->object_nl); if (state->array_nl) ruby_xfree(state->array_nl); - if (state->object_delim) fbuffer_free(state->object_delim); if (state->object_delim2) fbuffer_free(state->object_delim2); ruby_xfree(state); } @@ -624,7 +623,6 @@ static size_t State_memsize(const void *ptr) if (state->space_before) size += state->space_before_len + 1; if (state->object_nl) size += state->object_nl_len + 1; if (state->array_nl) size += state->array_nl_len + 1; - if (state->object_delim) size += FBUFFER_CAPA(state->object_delim); if (state->object_delim2) size += FBUFFER_CAPA(state->object_delim2); return size; } @@ -843,15 +841,13 @@ json_object_i(VALUE key, VALUE val, VALUE _arg) long object_nl_len = state->object_nl_len; char *indent = state->indent; long indent_len = state->indent_len; - char *delim = FBUFFER_PTR(state->object_delim); - long delim_len = FBUFFER_LEN(state->object_delim); char *delim2 = FBUFFER_PTR(state->object_delim2); long delim2_len = FBUFFER_LEN(state->object_delim2); long depth = state->depth; int j; VALUE klass, key_to_s; - if (arg->iter > 0) fbuffer_append(buffer, delim, delim_len); + if (arg->iter > 0) fbuffer_append_char(buffer, ','); if (object_nl) { fbuffer_append(buffer, object_nl, object_nl_len); } @@ -1070,12 +1066,6 @@ static FBuffer *cState_prepare_buffer(VALUE self) GET_STATE(self); buffer = fbuffer_alloc(state->buffer_initial_length); - if (state->object_delim) { - fbuffer_clear(state->object_delim); - } else { - state->object_delim = fbuffer_alloc(16); - } - fbuffer_append_char(state->object_delim, ','); if (state->object_delim2) { fbuffer_clear(state->object_delim2); } else { @@ -1163,7 +1153,6 @@ static VALUE cState_init_copy(VALUE obj, VALUE orig) objState->space_before = fstrndup(origState->space_before, origState->space_before_len); objState->object_nl = fstrndup(origState->object_nl, origState->object_nl_len); objState->array_nl = fstrndup(origState->array_nl, origState->array_nl_len); - if (origState->object_delim) objState->object_delim = fbuffer_dup(origState->object_delim); if (origState->object_delim2) objState->object_delim2 = fbuffer_dup(origState->object_delim2); return obj; } diff --git a/ext/json/ext/generator/generator.h b/ext/json/ext/generator/generator.h index ff93f210c..c1c779bf7 100644 --- a/ext/json/ext/generator/generator.h +++ b/ext/json/ext/generator/generator.h @@ -66,7 +66,6 @@ typedef struct JSON_Generator_StateStruct { long object_nl_len; char *array_nl; long array_nl_len; - FBuffer *object_delim; FBuffer *object_delim2; long max_nesting; char allow_nan; From 964f42e1f0440afe7f79f8957d95713a8938b7bf Mon Sep 17 00:00:00 2001 From: Yusuke Endoh Date: Wed, 27 Dec 2023 15:57:02 +0900 Subject: [PATCH 03/10] Stop prebuilding object_delim2 Also, remove static functions that are no longer used. This speeds up `JSON.generate` by about 5% in a benchmark. --- ext/json/ext/fbuffer/fbuffer.h | 15 ++++----------- ext/json/ext/generator/generator.c | 18 +++--------------- ext/json/ext/generator/generator.h | 1 - 3 files changed, 7 insertions(+), 27 deletions(-) diff --git a/ext/json/ext/fbuffer/fbuffer.h b/ext/json/ext/fbuffer/fbuffer.h index dc8f406b5..31eb5022b 100644 --- a/ext/json/ext/fbuffer/fbuffer.h +++ b/ext/json/ext/fbuffer/fbuffer.h @@ -59,14 +59,15 @@ typedef struct FBufferStruct { static FBuffer *fbuffer_alloc(unsigned long initial_length); static void fbuffer_free(FBuffer *fb); +#ifndef JSON_GENERATOR static void fbuffer_clear(FBuffer *fb); +#endif static void fbuffer_append(FBuffer *fb, const char *newstr, unsigned long len); #ifdef JSON_GENERATOR static void fbuffer_append_long(FBuffer *fb, long number); #endif static void fbuffer_append_char(FBuffer *fb, char newchr); #ifdef JSON_GENERATOR -static FBuffer *fbuffer_dup(FBuffer *fb); static VALUE fbuffer_to_s(FBuffer *fb); #endif @@ -86,10 +87,12 @@ static void fbuffer_free(FBuffer *fb) ruby_xfree(fb); } +#ifndef JSON_GENERATOR static void fbuffer_clear(FBuffer *fb) { fb->len = 0; } +#endif static void fbuffer_inc_capa(FBuffer *fb, unsigned long requested) { @@ -166,16 +169,6 @@ static void fbuffer_append_long(FBuffer *fb, long number) fbuffer_append(fb, buf, len); } -static FBuffer *fbuffer_dup(FBuffer *fb) -{ - unsigned long len = fb->len; - FBuffer *result; - - result = fbuffer_alloc(len); - fbuffer_append(result, FBUFFER_PAIR(fb)); - return result; -} - static VALUE fbuffer_to_s(FBuffer *fb) { VALUE result = rb_str_new(FBUFFER_PTR(fb), FBUFFER_LEN(fb)); diff --git a/ext/json/ext/generator/generator.c b/ext/json/ext/generator/generator.c index a25e77780..97831d0ac 100644 --- a/ext/json/ext/generator/generator.c +++ b/ext/json/ext/generator/generator.c @@ -610,7 +610,6 @@ static void State_free(void *ptr) if (state->space_before) ruby_xfree(state->space_before); if (state->object_nl) ruby_xfree(state->object_nl); if (state->array_nl) ruby_xfree(state->array_nl); - if (state->object_delim2) fbuffer_free(state->object_delim2); ruby_xfree(state); } @@ -623,7 +622,6 @@ static size_t State_memsize(const void *ptr) if (state->space_before) size += state->space_before_len + 1; if (state->object_nl) size += state->object_nl_len + 1; if (state->array_nl) size += state->array_nl_len + 1; - if (state->object_delim2) size += FBUFFER_CAPA(state->object_delim2); return size; } @@ -841,8 +839,6 @@ json_object_i(VALUE key, VALUE val, VALUE _arg) long object_nl_len = state->object_nl_len; char *indent = state->indent; long indent_len = state->indent_len; - char *delim2 = FBUFFER_PTR(state->object_delim2); - long delim2_len = FBUFFER_LEN(state->object_delim2); long depth = state->depth; int j; VALUE klass, key_to_s; @@ -867,7 +863,9 @@ json_object_i(VALUE key, VALUE val, VALUE _arg) } Check_Type(key_to_s, T_STRING); generate_json(buffer, Vstate, state, key_to_s); - fbuffer_append(buffer, delim2, delim2_len); + if (RB_UNLIKELY(state->space_before)) fbuffer_append(buffer, state->space_before, state->space_before_len); + fbuffer_append_char(buffer, ':'); + if (RB_UNLIKELY(state->space)) fbuffer_append(buffer, state->space, state->space_len); generate_json(buffer, Vstate, state, val); arg->iter++; @@ -1066,15 +1064,6 @@ static FBuffer *cState_prepare_buffer(VALUE self) GET_STATE(self); buffer = fbuffer_alloc(state->buffer_initial_length); - if (state->object_delim2) { - fbuffer_clear(state->object_delim2); - } else { - state->object_delim2 = fbuffer_alloc(16); - } - if (state->space_before) fbuffer_append(state->object_delim2, state->space_before, state->space_before_len); - fbuffer_append_char(state->object_delim2, ':'); - if (state->space) fbuffer_append(state->object_delim2, state->space, state->space_len); - return buffer; } @@ -1153,7 +1142,6 @@ static VALUE cState_init_copy(VALUE obj, VALUE orig) objState->space_before = fstrndup(origState->space_before, origState->space_before_len); objState->object_nl = fstrndup(origState->object_nl, origState->object_nl_len); objState->array_nl = fstrndup(origState->array_nl, origState->array_nl_len); - if (origState->object_delim2) objState->object_delim2 = fbuffer_dup(origState->object_delim2); return obj; } diff --git a/ext/json/ext/generator/generator.h b/ext/json/ext/generator/generator.h index c1c779bf7..c6c237096 100644 --- a/ext/json/ext/generator/generator.h +++ b/ext/json/ext/generator/generator.h @@ -66,7 +66,6 @@ typedef struct JSON_Generator_StateStruct { long object_nl_len; char *array_nl; long array_nl_len; - FBuffer *object_delim2; long max_nesting; char allow_nan; char ascii_only; From 8a800d3d52ae595f2025cd927b0ad5fb7281c8a2 Mon Sep 17 00:00:00 2001 From: Yusuke Endoh Date: Wed, 27 Dec 2023 16:06:17 +0900 Subject: [PATCH 04/10] Apply RB_UNLIKELY for less frequently used options This speeds up `JSON.generate` by about 4% in a benchmark. --- ext/json/ext/generator/generator.c | 42 +++++++++++------------------- 1 file changed, 15 insertions(+), 27 deletions(-) diff --git a/ext/json/ext/generator/generator.c b/ext/json/ext/generator/generator.c index 97831d0ac..90783f8fe 100644 --- a/ext/json/ext/generator/generator.c +++ b/ext/json/ext/generator/generator.c @@ -835,21 +835,17 @@ json_object_i(VALUE key, VALUE val, VALUE _arg) JSON_Generator_State *state = arg->state; VALUE Vstate = arg->Vstate; - char *object_nl = state->object_nl; - long object_nl_len = state->object_nl_len; - char *indent = state->indent; - long indent_len = state->indent_len; long depth = state->depth; int j; VALUE klass, key_to_s; if (arg->iter > 0) fbuffer_append_char(buffer, ','); - if (object_nl) { - fbuffer_append(buffer, object_nl, object_nl_len); + if (RB_UNLIKELY(state->object_nl)) { + fbuffer_append(buffer, state->object_nl, state->object_nl_len); } - if (indent) { + if (RB_UNLIKELY(state->indent)) { for (j = 0; j < depth; j++) { - fbuffer_append(buffer, indent, indent_len); + fbuffer_append(buffer, state->indent, state->indent_len); } } @@ -874,10 +870,6 @@ json_object_i(VALUE key, VALUE val, VALUE _arg) static void generate_json_object(FBuffer *buffer, VALUE Vstate, JSON_Generator_State *state, VALUE obj) { - char *object_nl = state->object_nl; - long object_nl_len = state->object_nl_len; - char *indent = state->indent; - long indent_len = state->indent_len; long max_nesting = state->max_nesting; long depth = ++state->depth; int j; @@ -896,11 +888,11 @@ static void generate_json_object(FBuffer *buffer, VALUE Vstate, JSON_Generator_S rb_hash_foreach(obj, json_object_i, (VALUE)&arg); depth = --state->depth; - if (object_nl) { - fbuffer_append(buffer, object_nl, object_nl_len); - if (indent) { + if (RB_UNLIKELY(state->object_nl)) { + fbuffer_append(buffer, state->object_nl, state->object_nl_len); + if (RB_UNLIKELY(state->indent)) { for (j = 0; j < depth; j++) { - fbuffer_append(buffer, indent, indent_len); + fbuffer_append(buffer, state->indent, state->indent_len); } } } @@ -909,10 +901,6 @@ static void generate_json_object(FBuffer *buffer, VALUE Vstate, JSON_Generator_S static void generate_json_array(FBuffer *buffer, VALUE Vstate, JSON_Generator_State *state, VALUE obj) { - char *array_nl = state->array_nl; - long array_nl_len = state->array_nl_len; - char *indent = state->indent; - long indent_len = state->indent_len; long max_nesting = state->max_nesting; long depth = ++state->depth; int i, j; @@ -921,25 +909,25 @@ static void generate_json_array(FBuffer *buffer, VALUE Vstate, JSON_Generator_St rb_raise(eNestingError, "nesting of %ld is too deep", --state->depth); } fbuffer_append_char(buffer, '['); - if (array_nl) fbuffer_append(buffer, array_nl, array_nl_len); + if (RB_UNLIKELY(state->array_nl)) fbuffer_append(buffer, state->array_nl, state->array_nl_len); for(i = 0; i < RARRAY_LEN(obj); i++) { if (i > 0) { fbuffer_append_char(buffer, ','); if (RB_UNLIKELY(state->array_nl)) fbuffer_append(buffer, state->array_nl, state->array_nl_len); } - if (indent) { + if (RB_UNLIKELY(state->indent)) { for (j = 0; j < depth; j++) { - fbuffer_append(buffer, indent, indent_len); + fbuffer_append(buffer, state->indent, state->indent_len); } } generate_json(buffer, Vstate, state, rb_ary_entry(obj, i)); } state->depth = --depth; - if (array_nl) { - fbuffer_append(buffer, array_nl, array_nl_len); - if (indent) { + if (RB_UNLIKELY(state->array_nl)) { + fbuffer_append(buffer, state->array_nl, state->array_nl_len); + if (RB_UNLIKELY(state->indent)) { for (j = 0; j < depth; j++) { - fbuffer_append(buffer, indent, indent_len); + fbuffer_append(buffer, state->indent, state->indent_len); } } } From a19e80bd901dae54b831f161f62c172e8f532d2d Mon Sep 17 00:00:00 2001 From: Yusuke Endoh Date: Wed, 27 Dec 2023 16:32:47 +0900 Subject: [PATCH 05/10] Use `RB_ENCODING_GET` instead of `rb_enc_get` to improve performance This speeds up `JSON.generate` by about 12% in a benchmark. --- ext/json/ext/generator/generator.c | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/ext/json/ext/generator/generator.c b/ext/json/ext/generator/generator.c index 90783f8fe..7deee63cc 100644 --- a/ext/json/ext/generator/generator.c +++ b/ext/json/ext/generator/generator.c @@ -934,11 +934,13 @@ static void generate_json_array(FBuffer *buffer, VALUE Vstate, JSON_Generator_St fbuffer_append_char(buffer, ']'); } +static int usascii_encindex, utf8_encindex; + #ifdef HAVE_RUBY_ENCODING_H -static int enc_utf8_compatible_p(rb_encoding *enc) +static int enc_utf8_compatible_p(int enc_idx) { - if (enc == rb_usascii_encoding()) return 1; - if (enc == rb_utf8_encoding()) return 1; + if (enc_idx == usascii_encindex) return 1; + if (enc_idx == utf8_encindex) return 1; return 0; } #endif @@ -947,7 +949,7 @@ static void generate_json_string(FBuffer *buffer, VALUE Vstate, JSON_Generator_S { fbuffer_append_char(buffer, '"'); #ifdef HAVE_RUBY_ENCODING_H - if (!enc_utf8_compatible_p(rb_enc_get(obj))) { + if (!enc_utf8_compatible_p(RB_ENCODING_GET(obj))) { obj = rb_str_export_to_enc(obj, rb_utf8_encoding()); } #endif @@ -1626,4 +1628,7 @@ void Init_generator(void) i_match = rb_intern("match"); i_keys = rb_intern("keys"); i_dup = rb_intern("dup"); + + usascii_encindex = rb_usascii_encindex(); + utf8_encindex = rb_utf8_encindex(); } From a81ec4770af4a2f20a9dc06d0295cf5b93a7af91 Mon Sep 17 00:00:00 2001 From: Yusuke Endoh Date: Wed, 27 Dec 2023 16:46:15 +0900 Subject: [PATCH 06/10] Pre-check that escaping is not required for string dump ... instead of `rb_enc_str_asciionly_p`. If escaping is not needed, we can use `fbuffer_append` directly, which is much faster. This speeds up `JSON.generate` by about 16% in a benchmark. --- ext/json/ext/generator/generator.c | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/ext/json/ext/generator/generator.c b/ext/json/ext/generator/generator.c index 7deee63cc..6928a93a3 100644 --- a/ext/json/ext/generator/generator.c +++ b/ext/json/ext/generator/generator.c @@ -224,6 +224,18 @@ static void convert_UTF8_to_JSON_ASCII(FBuffer *buffer, VALUE string, char scrip RB_GC_GUARD(string); } +static const char escapeTable[256] = { + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, + 0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, /* '"' and '/' */ + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0, /* '\\' */ + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 +}; + + /* Converts string to a JSON string in FBuffer buffer, where only the * characters required by the JSON standard are JSON escaped. The remaining * characters (should be UTF8) are just passed through and appended to the @@ -231,13 +243,20 @@ static void convert_UTF8_to_JSON_ASCII(FBuffer *buffer, VALUE string, char scrip static void convert_UTF8_to_JSON(FBuffer *buffer, VALUE string, char script_safe) { const char *ptr = RSTRING_PTR(string), *p; - unsigned long len = RSTRING_LEN(string), start = 0, end = 0; + unsigned long len = RSTRING_LEN(string), need_escape = 0, start = 0, end = 0; const char *escape = NULL; int escape_len; unsigned char c; char buf[6] = { '\\', 'u' }; - int ascii_only = rb_enc_str_asciionly_p(string); + for (p = ptr; (unsigned long)(p - ptr) < len;) { + need_escape |= escapeTable[(int)*p++]; + } + if (!need_escape) { + // fast path + fbuffer_append(buffer, ptr, len); + return; + } for (start = 0, end = 0; end < len;) { p = ptr + end; c = (unsigned char) *p; @@ -288,7 +307,7 @@ static void convert_UTF8_to_JSON(FBuffer *buffer, VALUE string, char script_safe default: { unsigned short clen = 1; - if (!ascii_only) { + if (c >= 0x80) { clen += trailingBytesForUTF8[c]; if (end + clen > len) { rb_raise(rb_path2class("JSON::GeneratorError"), From fc98d7f9ea752d109f4cb9401659132f97d69f87 Mon Sep 17 00:00:00 2001 From: Yusuke Endoh Date: Wed, 27 Dec 2023 16:51:04 +0900 Subject: [PATCH 07/10] Use efficient object-type dispatching Dispatching based on Ruby's VALUE structure is more efficient than simply cascaded "if ... else if ..." checks. This speeds up `JSON.generate` by about 5% in a benchmark. --- ext/json/ext/generator/generator.c | 67 ++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 23 deletions(-) diff --git a/ext/json/ext/generator/generator.c b/ext/json/ext/generator/generator.c index 6928a93a3..f348b0628 100644 --- a/ext/json/ext/generator/generator.c +++ b/ext/json/ext/generator/generator.c @@ -1035,35 +1035,56 @@ static void generate_json_float(FBuffer *buffer, VALUE Vstate, JSON_Generator_St static void generate_json(FBuffer *buffer, VALUE Vstate, JSON_Generator_State *state, VALUE obj) { VALUE tmp; - VALUE klass = CLASS_OF(obj); - if (klass == rb_cHash) { - generate_json_object(buffer, Vstate, state, obj); - } else if (klass == rb_cArray) { - generate_json_array(buffer, Vstate, state, obj); - } else if (klass == rb_cString) { - generate_json_string(buffer, Vstate, state, obj); - } else if (obj == Qnil) { + if (obj == Qnil) { generate_json_null(buffer, Vstate, state, obj); } else if (obj == Qfalse) { generate_json_false(buffer, Vstate, state, obj); } else if (obj == Qtrue) { generate_json_true(buffer, Vstate, state, obj); - } else if (FIXNUM_P(obj)) { - generate_json_fixnum(buffer, Vstate, state, obj); - } else if (RB_TYPE_P(obj, T_BIGNUM)) { - generate_json_bignum(buffer, Vstate, state, obj); - } else if (klass == rb_cFloat) { - generate_json_float(buffer, Vstate, state, obj); - } else if (state->strict) { - rb_raise(eGeneratorError, "%"PRIsVALUE" not allowed in JSON", RB_OBJ_STRING(CLASS_OF(obj))); - } else if (rb_respond_to(obj, i_to_json)) { - tmp = rb_funcall(obj, i_to_json, 1, Vstate); - Check_Type(tmp, T_STRING); - fbuffer_append_str(buffer, tmp); + } else if (RB_SPECIAL_CONST_P(obj)) { + if (RB_FIXNUM_P(obj)) { + generate_json_fixnum(buffer, Vstate, state, obj); + } else if (RB_FLONUM_P(obj)) { + generate_json_float(buffer, Vstate, state, obj); + } else { + goto general; + } } else { - tmp = rb_funcall(obj, i_to_s, 0); - Check_Type(tmp, T_STRING); - generate_json_string(buffer, Vstate, state, tmp); + VALUE klass = RBASIC_CLASS(obj); + switch (RB_BUILTIN_TYPE(obj)) { + case T_BIGNUM: + generate_json_bignum(buffer, Vstate, state, obj); + break; + case T_HASH: + if (klass != rb_cHash) goto general; + generate_json_object(buffer, Vstate, state, obj); + break; + case T_ARRAY: + if (klass != rb_cArray) goto general; + generate_json_array(buffer, Vstate, state, obj); + break; + case T_STRING: + if (klass != rb_cString) goto general; + generate_json_string(buffer, Vstate, state, obj); + break; + case T_FLOAT: + if (klass != rb_cFloat) goto general; + generate_json_float(buffer, Vstate, state, obj); + break; + default: + general: + if (state->strict) { + rb_raise(eGeneratorError, "%"PRIsVALUE" not allowed in JSON", RB_OBJ_STRING(CLASS_OF(obj))); + } else if (rb_respond_to(obj, i_to_json)) { + tmp = rb_funcall(obj, i_to_json, 1, Vstate); + Check_Type(tmp, T_STRING); + fbuffer_append_str(buffer, tmp); + } else { + tmp = rb_funcall(obj, i_to_s, 0); + Check_Type(tmp, T_STRING); + generate_json_string(buffer, Vstate, state, tmp); + } + } } } From 2914e35135e7ee9765e3d8b60b1034bcd6b86f85 Mon Sep 17 00:00:00 2001 From: Yusuke Endoh Date: Wed, 27 Dec 2023 16:55:03 +0900 Subject: [PATCH 08/10] Directly use `generate_json_string` for object keys ... instead of `generate_json`. Since the object key is already confirmed to be a string, using a generic dispatch function brings an unnecessary overhead. This speeds up `JSON.generate` by about 3% in a benchmark. --- ext/json/ext/generator/generator.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/json/ext/generator/generator.c b/ext/json/ext/generator/generator.c index f348b0628..a3fc6e5e5 100644 --- a/ext/json/ext/generator/generator.c +++ b/ext/json/ext/generator/generator.c @@ -877,7 +877,7 @@ json_object_i(VALUE key, VALUE val, VALUE _arg) key_to_s = rb_funcall(key, i_to_s, 0); } Check_Type(key_to_s, T_STRING); - generate_json(buffer, Vstate, state, key_to_s); + generate_json_string(buffer, Vstate, state, key_to_s); if (RB_UNLIKELY(state->space_before)) fbuffer_append(buffer, state->space_before, state->space_before_len); fbuffer_append_char(buffer, ':'); if (RB_UNLIKELY(state->space)) fbuffer_append(buffer, state->space, state->space_len); From 946cefe04628f2d9fa0ca69359bb8ae446680cbc Mon Sep 17 00:00:00 2001 From: Yusuke Endoh Date: Wed, 27 Dec 2023 16:57:00 +0900 Subject: [PATCH 09/10] Use `RARRAY_AREF` instead of `rb_ary_entry` to improve performance It is safe to use `RARRAY_AREF` here because no Ruby code is executed between `RARRAY_LEN` and `RARRAY_AREF`. This speeds up `JSON.generate` by about 4% in a benchmark. --- ext/json/ext/generator/generator.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/json/ext/generator/generator.c b/ext/json/ext/generator/generator.c index a3fc6e5e5..4e4d68868 100644 --- a/ext/json/ext/generator/generator.c +++ b/ext/json/ext/generator/generator.c @@ -939,7 +939,7 @@ static void generate_json_array(FBuffer *buffer, VALUE Vstate, JSON_Generator_St fbuffer_append(buffer, state->indent, state->indent_len); } } - generate_json(buffer, Vstate, state, rb_ary_entry(obj, i)); + generate_json(buffer, Vstate, state, RARRAY_AREF(obj, i)); } state->depth = --depth; if (RB_UNLIKELY(state->array_nl)) { From da58b625d17c30fb93833480eb10b919bc8d9704 Mon Sep 17 00:00:00 2001 From: Yusuke Endoh Date: Wed, 27 Dec 2023 17:11:22 +0900 Subject: [PATCH 10/10] Define dummy `RB_UNLIKELY` for old ruby versions --- ext/json/ext/generator/generator.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ext/json/ext/generator/generator.c b/ext/json/ext/generator/generator.c index 4e4d68868..d2a5a9717 100644 --- a/ext/json/ext/generator/generator.c +++ b/ext/json/ext/generator/generator.c @@ -18,6 +18,10 @@ static ID i_to_s, i_to_json, i_new, i_indent, i_space, i_space_before, i_aref, i_send, i_respond_to_p, i_match, i_keys, i_depth, i_buffer_initial_length, i_dup, i_script_safe, i_escape_slash, i_strict; +#ifndef RB_UNLIKELY +#define RB_UNLIKELY(cond) (cond) +#endif + /* * Copyright 2001-2004 Unicode, Inc. *