diff options
author | Jean Boussier <[email protected]> | 2024-10-29 11:00:16 +0100 |
---|---|---|
committer | Hiroshi SHIBATA <[email protected]> | 2024-11-01 13:04:24 +0900 |
commit | 59eebeca02325861dd16452c9b85f4920bccd84f (patch) | |
tree | dcc4bd7fde4a87782599354843ea788870285296 | |
parent | d329896fb590de96832d522b475404cae1279767 (diff) |
[ruby/json] Allocate the initial generator buffer on the stack
Ref: https://github.com/ruby/json/issues/655
Followup: https://github.com/ruby/json/issues/657
Assuming the generator might be used for fairly small documents
we can start with a reasonable buffer size of the stack, and if
we outgrow it, we can spill on the heap.
In a way this is optimizing for micro-benchmarks, but there are
valid use case for fiarly small JSON document in actual real world
scenarios, so trashing the GC less in such case make sense.
Before:
```
ruby 3.3.4 (2024-07-09 revision https://github.com/ruby/json/commit/be1089c8ec) +YJIT [arm64-darwin23]
Warming up --------------------------------------
Oj 518.700k i/100ms
JSON reuse 483.370k i/100ms
Calculating -------------------------------------
Oj 5.722M (± 1.8%) i/s (174.76 ns/i) - 29.047M in 5.077823s
JSON reuse 5.278M (± 1.5%) i/s (189.46 ns/i) - 26.585M in 5.038172s
Comparison:
Oj: 5722283.8 i/s
JSON reuse: 5278061.7 i/s - 1.08x slower
```
After:
```
ruby 3.3.4 (2024-07-09 revision https://github.com/ruby/json/commit/be1089c8ec) +YJIT [arm64-darwin23]
Warming up --------------------------------------
Oj 517.837k i/100ms
JSON reuse 548.871k i/100ms
Calculating -------------------------------------
Oj 5.693M (± 1.6%) i/s (175.65 ns/i) - 28.481M in 5.004056s
JSON reuse 5.855M (± 1.2%) i/s (170.80 ns/i) - 29.639M in 5.063004s
Comparison:
Oj: 5692985.6 i/s
JSON reuse: 5854857.9 i/s - 1.03x faster
```
https://github.com/ruby/json/commit/fe607f4806
-rw-r--r-- | ext/json/fbuffer/fbuffer.h | 29 | ||||
-rw-r--r-- | ext/json/generator/generator.c | 3 | ||||
-rw-r--r-- | ext/json/generator/generator.h | 1 | ||||
-rw-r--r-- | ext/json/parser/parser.c | 2 | ||||
-rw-r--r-- | ext/json/parser/parser.rl | 2 |
5 files changed, 30 insertions, 7 deletions
diff --git a/ext/json/fbuffer/fbuffer.h b/ext/json/fbuffer/fbuffer.h index dc09dde9cb..55fc0bba9d 100644 --- a/ext/json/fbuffer/fbuffer.h +++ b/ext/json/fbuffer/fbuffer.h @@ -4,13 +4,20 @@ #include "ruby.h" #include "ruby/encoding.h" +enum fbuffer_type { + HEAP = 0, + STACK = 1, +}; + typedef struct FBufferStruct { + enum fbuffer_type type; unsigned long initial_length; - char *ptr; unsigned long len; unsigned long capa; + char *ptr; } FBuffer; +#define FBUFFER_STACK_SIZE 512 #define FBUFFER_INITIAL_LENGTH_DEFAULT 1024 #define FBUFFER_PTR(fb) ((fb)->ptr) @@ -35,14 +42,21 @@ static VALUE fbuffer_to_s(FBuffer *fb); #define RB_UNLIKELY(expr) expr #endif -static void fbuffer_init(FBuffer *fb, unsigned long initial_length) +static void fbuffer_stack_init(FBuffer *fb, unsigned long initial_length, char *stack_buffer, long stack_buffer_size) { fb->initial_length = (initial_length > 0) ? initial_length : FBUFFER_INITIAL_LENGTH_DEFAULT; + if (stack_buffer) { + fb->type = STACK; + fb->ptr = stack_buffer; + fb->capa = stack_buffer_size; + } } static void fbuffer_free(FBuffer *fb) { - if (fb->ptr) ruby_xfree(fb->ptr); + if (fb->ptr && fb->type == HEAP) { + ruby_xfree(fb->ptr); + } } #ifndef JSON_GENERATOR @@ -65,7 +79,14 @@ static inline void fbuffer_inc_capa(FBuffer *fb, unsigned long requested) for (required = fb->capa; requested > required - fb->len; required <<= 1); if (required > fb->capa) { - REALLOC_N(fb->ptr, char, required); + if (fb->type == STACK) { + const char *old_buffer = fb->ptr; + fb->ptr = ALLOC_N(char, required); + fb->type = HEAP; + MEMCPY(fb->ptr, old_buffer, char, fb->len); + } else { + REALLOC_N(fb->ptr, char, required); + } fb->capa = required; } } diff --git a/ext/json/generator/generator.c b/ext/json/generator/generator.c index 4e1c0e314e..fc002ff737 100644 --- a/ext/json/generator/generator.c +++ b/ext/json/generator/generator.c @@ -954,8 +954,9 @@ static VALUE cState_partial_generate(VALUE self, VALUE obj, void (*func)(FBuffer { GET_STATE(self); + char stack_buffer[FBUFFER_STACK_SIZE]; FBuffer buffer = {0}; - fbuffer_init(&buffer, state->buffer_initial_length); + fbuffer_stack_init(&buffer, state->buffer_initial_length, stack_buffer, FBUFFER_STACK_SIZE); struct generate_json_data data = { .buffer = &buffer, diff --git a/ext/json/generator/generator.h b/ext/json/generator/generator.h index 05e8f0d809..b6898c955f 100644 --- a/ext/json/generator/generator.h +++ b/ext/json/generator/generator.h @@ -54,6 +54,7 @@ typedef struct JSON_Generator_StateStruct { JSON_Generator_State *state; \ GET_STATE_TO(self, state) + static VALUE mHash_to_json(int argc, VALUE *argv, VALUE self); static VALUE mArray_to_json(int argc, VALUE *argv, VALUE self); #ifdef RUBY_INTEGER_UNIFICATION diff --git a/ext/json/parser/parser.c b/ext/json/parser/parser.c index 368d539bcd..a6d8ff2cc5 100644 --- a/ext/json/parser/parser.c +++ b/ext/json/parser/parser.c @@ -2159,7 +2159,7 @@ static VALUE cJSON_parser_s_allocate(VALUE klass) { JSON_Parser *json; VALUE obj = TypedData_Make_Struct(klass, JSON_Parser, &JSON_Parser_type, json); - fbuffer_init(&json->fbuffer, 0); + fbuffer_stack_init(&json->fbuffer, 0, NULL, 0); return obj; } diff --git a/ext/json/parser/parser.rl b/ext/json/parser/parser.rl index c627014593..499cc693a3 100644 --- a/ext/json/parser/parser.rl +++ b/ext/json/parser/parser.rl @@ -919,7 +919,7 @@ static VALUE cJSON_parser_s_allocate(VALUE klass) { JSON_Parser *json; VALUE obj = TypedData_Make_Struct(klass, JSON_Parser, &JSON_Parser_type, json); - fbuffer_init(&json->fbuffer, 0); + fbuffer_stack_init(&json->fbuffer, 0, NULL, 0); return obj; } |