This is an automated email from the ASF dual-hosted git repository.

nickva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/couchdb-jiffy.git

commit 172c8bdc252d6eecb76b92cfdd3235ee27c9cf27
Author: Nick Vatamaniuc <[email protected]>
AuthorDate: Sun Jun 14 01:06:04 2026 -0400

     Reduce encoder memory allocations
    
    Flush the buffer into iolist segments with a chunk size that doubles (up to
    64KB). Large documents should now have O(log n) allocation.
    
    We can use tprof to check allocation with OTP 27+. Example of memory 
profiling
    before and after with `canada.json` from bench data:
    
    ```
    {ok, B} = file:read_file("data/canada.json").
    T = jiffy:decode(B, [return_maps]).
    tprof:profile(jiffy, encode, [T], #{type => call_memory}).
    ```
    
    Before
    ```
    ****** Process <0.114.0>  --  100.00% of total ***
    FUNCTION                 CALLS  WORDS  PER CALL  [    %]
    lists:reverse/1              1      4      4.00  [ 0.03]
    lists:reverse/2              1   2062   2062.00  [14.21]
    jiffy:nif_encode_init/2      1  12441  12441.00  [85.76]
                                    14507            [100.0]
    ```
    
    After
    ```
    ****** Process <0.101.0>  --  100.00% of total ***
    FUNCTION                 CALLS  WORDS  PER CALL  [    %]
    lists:reverse/1              1      4      4.00  [ 0.29]
    lists:reverse/2              1     68     68.00  [ 4.88]
    jiffy:nif_encode_init/2      1   1321   1321.00  [94.83]
    ```
    
    This shows a good speed improvment on some benchmarks especially ones with
    larger objects. Saw about ~6% speedup on on object-heavy benchmarks
    (citm, pokedex, github):. Larger ones like "canada" showed about a ~1.25x
    speedup.
---
 c_src/encoder.c | 17 +++++++++++++++--
 1 file changed, 15 insertions(+), 2 deletions(-)

diff --git a/c_src/encoder.c b/c_src/encoder.c
index 413ce79..fa4bf98 100644
--- a/c_src/encoder.c
+++ b/c_src/encoder.c
@@ -11,6 +11,12 @@
 
 #define BIN_INC_SIZE 2048
 
+// When we flush output buffer to an iolist double the next chunk size so a
+// large doc is emitted in O(log n) segments. But we also limit it at 64KB
+// size. This should reduce total number of memory allocations (those can
+// become a bottleneck).
+#define MAX_CHUNK_SIZE (64 * 1024)
+
 #define MIN(X, Y) ((X) < (Y) ? (X) : (Y))
 
 #define SMALL_TERMSTACK_SIZE 16
@@ -51,6 +57,7 @@ typedef struct {
 
     ErlNifBinary    buffer;
     int             have_buffer;
+    size_t          chunk_size;
 
     unsigned char*  p;
     size_t          i;
@@ -169,8 +176,9 @@ enc_new(ErlNifEnv* env)
     e->atoms = st;
     e->bytes_per_red = DEFAULT_BYTES_PER_REDUCTION;
     e->iolist = enif_make_list(env, 0);
+    e->chunk_size = BIN_INC_SIZE;
 
-    if(!enif_alloc_binary(BIN_INC_SIZE, &e->buffer)) {
+    if(!enif_alloc_binary(e->chunk_size, &e->buffer)) {
         enif_release_resource(e);
         return NULL;
     }
@@ -246,6 +254,11 @@ enc_flush(Encoder* e)
     e->iolist = enif_make_list_cell(e->env, bin, e->iolist);
     e->iosize += e->i;
 
+    // Grow the next chunk geometrically so large outputs flush O(log n) times.
+    if(e->chunk_size < MAX_CHUNK_SIZE) {
+        e->chunk_size <<= 1;
+    }
+
     return 1;
 }
 
@@ -268,7 +281,7 @@ enc_ensure(Encoder* e, size_t req)
         }
     }
 
-    for(new_size = BIN_INC_SIZE; new_size < req; new_size <<= 1);
+    for(new_size = e->chunk_size; new_size < req; new_size <<= 1);
 
     if(!enif_alloc_binary(new_size, &e->buffer)) {
         return 0;

Reply via email to