You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by va...@apache.org on 2026/04/21 04:09:31 UTC

(couchdb-jiffy) 20/49: Plain ASCII scan ahead optimization in string encoding

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 f8a7c77b6ba79de515162d7c5207a36e91fa3d4d
Author: Nick Vatamaniuc <va...@gmail.com>
AuthorDate: Thu Apr 2 01:02:31 2026 -0400

    Plain ASCII scan ahead optimization in string encoding
    
    What's good for the gander is good for the ... duck?
    
    Apply the same lookahead optimization from string [1] and number decoding [2] to string
    and atom encoding. Hoist the index update (`e->i`) out of the loop and mark the
    source pointer as restricted. Another good perf boost comes from a single final
    `memcpy` instead of updating the buffer a byte at a time.
    
    As a curiosity, while the compiler doesn't do any fancy auto-vectorization in
    the while loop, it manages to use a single bitmask check instruction (bt) to
    check for all three special cases `"`, `/` and '\' which is kind of neat:
    
    ```c
     i++;
     while(i < size && p[i] >= 0x20 && p[i] < 0x80
           && p[i] != '\"' && p[i] != '\\' && p[i] != '/'
      ) {i++;}
    ```
    
    AT&T syntax (sorry)
    ```
    leaq    1(%r15), %rdx               ## i++
    movzbl  %dil, %r8d                  ## r8d contains p[i]
    addl    $-34, %r8d                  ## r8d -= '"'
    cmpl    $58, %r8d                   ## if r8d - '\' > 0
    ja      LBB12_23                    ## then jump away
    movabsq $288230376151719937, %rdi   ## imm = 0x400000000002001
    btq     %r8, %rdi                   ## if bitmask matches " / and \
    jae     LBB12_23                    ## then jump away
    ```
    
    Using the bench benchee benchmark [3], this optimization shows decent results
    for the encoding side. For the long strings case (issue 90) we get a 5x(!)
    speedup. Some like GovTrack and Giphy get about a 30-40% boost. Others are
    lower, and Canada, since it's number heavy (coordinates), unsurprisingly
    doesn't change much.
    
    ===== Master =====
    
    ```
    ===== With input Blockchain =====
    Name               ips        average  deviation         median         99th %
    jiffy           8.58 K      116.54 us    +/-50.23%      109.87 us      199.99 us
    
    ===== With input Canada =====
    Name               ips        average  deviation         median         99th %
    jiffy            32.70       30.58 ms     +/-2.13%       30.51 ms       34.36 ms
    
    ===== With input Giphy =====
    Name               ips        average  deviation         median         99th %
    jiffy           780.56        1.28 ms    +/-14.11%        1.23 ms        1.65 ms
    
    ===== With input GitHub =====
    Name               ips        average  deviation         median         99th %
    jiffy           2.35 K        0.43 ms    +/-14.52%        0.40 ms        0.60 ms
    
    ===== With input GovTrack =====
    Name               ips        average  deviation         median         99th %
    jiffy            26.39       37.89 ms     +/-3.95%       37.77 ms       47.00 ms
    
    ===== With input Issue 90 =====
    Name               ips        average  deviation         median         99th %
    jiffy            15.97       62.60 ms     +/-6.27%       61.84 ms       96.36 ms
    
    ===== With input JSON Generator =====
    Name               ips        average  deviation         median         99th %
    jiffy           775.51        1.29 ms    +/-12.40%        1.32 ms        1.81 ms
    
    ===== With input Pokedex =====
    Name               ips        average  deviation         median         99th %
    jiffy           1.35 K        0.74 ms     +/-9.76%        0.74 ms        0.95 ms
    
    ===== With input UTF-8 unescaped =====
    Name               ips        average  deviation         median         99th %
    jiffy           8.20 K      121.88 us     +/-7.75%      119.79 us      161.74 us
    ```
    
    ===== PR =====
    ```
    ===== With input Blockchain =====
    Name               ips        average  deviation         median         99th %
    jiffy           9.86 K      101.46 us    +/-13.45%       96.60 us      162.25 us
    
    ===== With input Canada =====
    Name               ips        average  deviation         median         99th %
    jiffy            31.41       31.84 ms    +/-10.76%       31.33 ms       58.22 ms
    
    ===== With input Giphy =====
    Name               ips        average  deviation         median         99th %
    jiffy           1.03 K        0.97 ms    +/-17.42%        0.91 ms        1.38 ms
    
    ===== With input GitHub =====
    Name               ips        average  deviation         median         99th %
    jiffy           3.17 K        0.32 ms    +/-21.45%        0.28 ms        0.51 ms
    
    ===== With input GovTrack =====
    Name               ips        average  deviation         median         99th %
    jiffy            39.19       25.51 ms    +/-14.82%       23.80 ms       37.69 ms
    
    ===== With input Issue 90 =====
    Name               ips        average  deviation         median         99th %
    jiffy            84.84       11.79 ms     +/-3.38%       11.71 ms       14.03 ms
    
    ===== With input JSON Generator =====
    Name               ips        average  deviation         median         99th %
    jiffy           1.10 K        0.91 ms    +/-21.32%        0.85 ms        1.55 ms
    
    ===== With input Pokedex =====
    Name               ips        average  deviation         median         99th %
    jiffy           1.44 K        0.70 ms    +/-48.81%        0.63 ms        1.69 ms
    
    ===== With input UTF-8 unescaped =====
    Name               ips        average  deviation         median         99th %
    jiffy           9.19 K      108.82 us     +/-7.33%      107.02 us      141.75 us
    ```
    
    [1] https://github.com/davisp/jiffy/pull/252
    [2] https://github.com/davisp/jiffy/pull/254
    [2] https://github.com/nickva/bench
---
 c_src/encoder.c | 43 ++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 40 insertions(+), 3 deletions(-)

diff --git a/c_src/encoder.c b/c_src/encoder.c
index 0cf1b08..96f2c24 100644
--- a/c_src/encoder.c
+++ b/c_src/encoder.c
@@ -360,6 +360,7 @@ enc_atom(Encoder* e, ERL_NIF_TERM val)
     unsigned char data[512];
 
     size_t size;
+    size_t start;
     size_t i;
 
     if(!enif_get_atom(e->env, val, (char*)data, 512, ERL_NIF_LATIN1)) {
@@ -384,8 +385,25 @@ enc_atom(Encoder* e, ERL_NIF_TERM val)
         if(enc_special_character(e, data[i])) {
             i++;
         } else if(data[i] < 0x80) {
-            e->p[e->i++] = data[i];
+            // Scan ahead for plain ASCII chars which don't need escaping.
+            // Since optionally users could escape forward slashes, too, we
+            // stop on them as well
+            start = i;
             i++;
+            while(i < size
+                    && data[i] >= 0x20
+                    && data[i] < 0x80
+                    && data[i] != '\"'
+                    && data[i] != '\\'
+                    && data[i] != '/') {
+                i++;
+            }
+            size_t run = i - start;
+            if(!enc_ensure(e, run)) {
+                return 0;
+            }
+            memcpy(&(e->p[e->i]), &data[start], run);
+            e->i += run;
         } else if(data[i] >= 0x80) {
             /* The atom encoding is latin1, so we don't need validation
              * as all latin1 characters are valid Unicode codepoints. */
@@ -415,11 +433,12 @@ enc_string(Encoder* e, ERL_NIF_TERM val)
     static const int MAX_ESCAPE_LEN = 12;
     ErlNifBinary bin;
 
-    unsigned char* data;
+    unsigned char* JIFFY_RESTRICT data;
     size_t size;
     int esc_len;
     size_t ulen;
     int uval;
+    size_t start;
     size_t i;
 
     if(!enif_inspect_binary(e->env, val, &bin)) {
@@ -445,7 +464,25 @@ enc_string(Encoder* e, ERL_NIF_TERM val)
         if(enc_special_character(e, data[i])) {
             i++;
         } else if(data[i] < 0x80) {
-            e->p[e->i++] = data[i++];
+            // Scan ahead for plain ASCII char and memcpy them. Stop at quotes,
+            // backslashes, and forward slashes, since users can optionally
+            // choose to escape them too.
+            start = i;
+            i++;
+            while(i < size
+                    && data[i] >= 0x20
+                    && data[i] < 0x80
+                    && data[i] != '\"'
+                    && data[i] != '\\'
+                    && data[i] != '/') {
+                i++;
+            }
+            size_t run = i - start;
+            if(!enc_ensure(e, run)) {
+                return 0;
+            }
+            memcpy(&(e->p[e->i]), &data[start], run);
+            e->i += run;
         } else if(data[i] >= 0x80) {
             ulen = utf8_validate(&(data[i]), size - i);