benchmark encoders and speed them up

This commit is contained in:
2026-02-17 12:36:07 -06:00
parent 2d054fcf21
commit 8f9eb0aaa9
4 changed files with 520 additions and 59 deletions

405
benches/encoders.cm Normal file
View File

@@ -0,0 +1,405 @@
// encoders.cm — nota/wota/json encode+decode benchmark
// Isolates per-type bottlenecks across all three serializers.
var nota = use('nota')
var wota = use('wota')
var json = use('json')
// --- Test data shapes ---
// Small integers: fast path for all encoders
var integers_small = array(100, function(i) { return i + 1 })
// Floats: stresses nota's snprintf path
var floats_array = array(100, function(i) {
return 3.14159 * (i + 1) + 0.00001 * i
})
// Short strings in records: per-string overhead, property enumeration
var strings_short = array(50, function(i) {
var r = {}
r[`k${i}`] = `value_${i}`
return r
})
// Single long string: throughput test (wota byte loop, nota kim)
var long_str = ""
var li = 0
for (li = 0; li < 100; li++) {
long_str = `${long_str}abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMN`
}
var strings_long = long_str
// Unicode text: nota's kim encoding, wota's byte packing
var strings_unicode = "こんにちは世界 🌍🌎🌏 Ñoño café résumé naïve Ω∑∏ 你好世界"
// Nested records: cycle detection, property enumeration
function make_nested(depth, breadth) {
var obj = {}
var i = 0
var k = null
if (depth <= 0) {
for (i = 0; i < breadth; i++) {
k = `v${i}`
obj[k] = i * 2.5
}
return obj
}
for (i = 0; i < breadth; i++) {
k = `n${i}`
obj[k] = make_nested(depth - 1, breadth)
}
return obj
}
var nested_records = make_nested(3, 4)
// Flat record: property enumeration cost
var flat_record = {}
var fi = 0
for (fi = 0; fi < 50; fi++) {
flat_record[`prop_${fi}`] = fi * 1.1
}
// Mixed payload: realistic workload
var mixed_payload = array(50, function(i) {
var r = {}
r.id = i
r.name = `item_${i}`
r.active = i % 2 == 0
r.score = i * 3.14
r.tags = [`t${i % 5}`, `t${(i + 1) % 5}`]
return r
})
// --- Pre-encode for decode benchmarks ---
var nota_enc_integers = nota.encode(integers_small)
var nota_enc_floats = nota.encode(floats_array)
var nota_enc_strings_short = nota.encode(strings_short)
var nota_enc_strings_long = nota.encode(strings_long)
var nota_enc_strings_unicode = nota.encode(strings_unicode)
var nota_enc_nested = nota.encode(nested_records)
var nota_enc_flat = nota.encode(flat_record)
var nota_enc_mixed = nota.encode(mixed_payload)
var wota_enc_integers = wota.encode(integers_small)
var wota_enc_floats = wota.encode(floats_array)
var wota_enc_strings_short = wota.encode(strings_short)
var wota_enc_strings_long = wota.encode(strings_long)
var wota_enc_strings_unicode = wota.encode(strings_unicode)
var wota_enc_nested = wota.encode(nested_records)
var wota_enc_flat = wota.encode(flat_record)
var wota_enc_mixed = wota.encode(mixed_payload)
var json_enc_integers = json.encode(integers_small)
var json_enc_floats = json.encode(floats_array)
var json_enc_strings_short = json.encode(strings_short)
var json_enc_strings_long = json.encode(strings_long)
var json_enc_strings_unicode = json.encode(strings_unicode)
var json_enc_nested = json.encode(nested_records)
var json_enc_flat = json.encode(flat_record)
var json_enc_mixed = json.encode(mixed_payload)
// --- Benchmark functions ---
return {
// NOTA encode
nota_encode_integers: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = nota.encode(integers_small) }
return r
},
nota_encode_floats: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = nota.encode(floats_array) }
return r
},
nota_encode_strings_short: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = nota.encode(strings_short) }
return r
},
nota_encode_strings_long: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = nota.encode(strings_long) }
return r
},
nota_encode_strings_unicode: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = nota.encode(strings_unicode) }
return r
},
nota_encode_nested: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = nota.encode(nested_records) }
return r
},
nota_encode_flat: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = nota.encode(flat_record) }
return r
},
nota_encode_mixed: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = nota.encode(mixed_payload) }
return r
},
// NOTA decode
nota_decode_integers: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = nota.decode(nota_enc_integers) }
return r
},
nota_decode_floats: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = nota.decode(nota_enc_floats) }
return r
},
nota_decode_strings_short: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = nota.decode(nota_enc_strings_short) }
return r
},
nota_decode_strings_long: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = nota.decode(nota_enc_strings_long) }
return r
},
nota_decode_strings_unicode: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = nota.decode(nota_enc_strings_unicode) }
return r
},
nota_decode_nested: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = nota.decode(nota_enc_nested) }
return r
},
nota_decode_flat: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = nota.decode(nota_enc_flat) }
return r
},
nota_decode_mixed: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = nota.decode(nota_enc_mixed) }
return r
},
// WOTA encode
wota_encode_integers: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = wota.encode(integers_small) }
return r
},
wota_encode_floats: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = wota.encode(floats_array) }
return r
},
wota_encode_strings_short: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = wota.encode(strings_short) }
return r
},
wota_encode_strings_long: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = wota.encode(strings_long) }
return r
},
wota_encode_strings_unicode: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = wota.encode(strings_unicode) }
return r
},
wota_encode_nested: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = wota.encode(nested_records) }
return r
},
wota_encode_flat: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = wota.encode(flat_record) }
return r
},
wota_encode_mixed: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = wota.encode(mixed_payload) }
return r
},
// WOTA decode
wota_decode_integers: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = wota.decode(wota_enc_integers) }
return r
},
wota_decode_floats: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = wota.decode(wota_enc_floats) }
return r
},
wota_decode_strings_short: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = wota.decode(wota_enc_strings_short) }
return r
},
wota_decode_strings_long: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = wota.decode(wota_enc_strings_long) }
return r
},
wota_decode_strings_unicode: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = wota.decode(wota_enc_strings_unicode) }
return r
},
wota_decode_nested: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = wota.decode(wota_enc_nested) }
return r
},
wota_decode_flat: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = wota.decode(wota_enc_flat) }
return r
},
wota_decode_mixed: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = wota.decode(wota_enc_mixed) }
return r
},
// JSON encode
json_encode_integers: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = json.encode(integers_small) }
return r
},
json_encode_floats: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = json.encode(floats_array) }
return r
},
json_encode_strings_short: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = json.encode(strings_short) }
return r
},
json_encode_strings_long: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = json.encode(strings_long) }
return r
},
json_encode_strings_unicode: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = json.encode(strings_unicode) }
return r
},
json_encode_nested: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = json.encode(nested_records) }
return r
},
json_encode_flat: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = json.encode(flat_record) }
return r
},
json_encode_mixed: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = json.encode(mixed_payload) }
return r
},
// JSON decode
json_decode_integers: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = json.decode(json_enc_integers) }
return r
},
json_decode_floats: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = json.decode(json_enc_floats) }
return r
},
json_decode_strings_short: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = json.decode(json_enc_strings_short) }
return r
},
json_decode_strings_long: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = json.decode(json_enc_strings_long) }
return r
},
json_decode_strings_unicode: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = json.decode(json_enc_strings_unicode) }
return r
},
json_decode_nested: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = json.decode(json_enc_nested) }
return r
},
json_decode_flat: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = json.decode(json_enc_flat) }
return r
},
json_decode_mixed: function(n) {
var i = 0
var r = null
for (i = 0; i < n; i++) { r = json.decode(json_enc_mixed) }
return r
}
}

View File

@@ -308,6 +308,30 @@ void nota_write_blob(NotaBuffer *nb, unsigned long long nbits, const char *data)
void nota_write_text(NotaBuffer *nb, const char *s)
{
/* ASCII fast path: if all bytes < 0x80, KIM == UTF-8 and rune count == byte count */
size_t slen = strlen(s);
const unsigned char *scan = (const unsigned char *)s;
int is_ascii = 1;
for (size_t k = 0; k < slen; k++) {
if (scan[k] >= 0x80) {
is_ascii = 0;
break;
}
}
if (is_ascii) {
long long runes = (long long)slen;
char *p = nota_buffer_alloc(nb, 1 + 10 + slen);
p[0] = NOTA_TEXT;
char *end = nota_continue_num(runes, p, 4);
memcpy(end, s, slen);
size_t used = (size_t)(end - p) + slen;
size_t allocated = 1 + 10 + slen;
nb->size -= (allocated - used);
return;
}
/* Non-ASCII path: full UTF-8 decode + KIM encode */
long long runes = utf8_count(s);
size_t max_kim = (size_t)(runes * 5);
@@ -367,6 +391,13 @@ static void nota_write_int_buf(NotaBuffer *nb, long long n)
nb->size -= (10 - used);
}
static const double nota_pow10_table[29] = {
1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7,
1e8, 1e9, 1e10, 1e11, 1e12, 1e13, 1e14, 1e15,
1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22, 1e23,
1e24, 1e25, 1e26, 1e27, 1e28
};
static void extract_mantissa_coefficient(double num, long *coefficient, long *exponent)
{
if (num == 0.0) {
@@ -375,32 +406,46 @@ static void extract_mantissa_coefficient(double num, long *coefficient, long *ex
return;
}
/* Round to 12 decimal places to avoid floating artifacts. */
double rounded = floor(fabs(num) * 1e12 + 0.5) / 1e12;
if (num < 0) {
rounded = -rounded;
double absval = fabs(num);
int sign = (num < 0) ? -1 : 1;
/* Get decimal exponent via log10 */
int dec_exp = (int)floor(log10(absval));
/* Scale to extract 14-digit coefficient.
We want coeff * 10^exp = absval, with coeff having up to 14 digits.
So coeff = absval * 10^(13 - dec_exp), exp = dec_exp - 13 */
int shift = 13 - dec_exp;
double scaled;
if (shift >= 0 && shift <= 28) {
scaled = absval * nota_pow10_table[shift];
} else if (shift < 0 && -shift <= 28) {
scaled = absval / nota_pow10_table[-shift];
} else {
scaled = absval * pow(10.0, (double)shift);
}
char buf[64];
snprintf(buf, sizeof(buf), "%.14g", rounded);
long long coeff = (long long)(scaled + 0.5);
char *exp_pos = strpbrk(buf, "eE");
long exp_from_sci = 0;
if (exp_pos) {
exp_from_sci = atol(exp_pos + 1);
*exp_pos = '\0';
/* Correct off-by-one from log10 rounding */
if (coeff >= 100000000000000LL) {
coeff = (coeff + 5) / 10;
shift--;
} else if (coeff < 10000000000000LL && coeff > 0) {
coeff = (long long)(absval * pow(10.0, (double)(shift + 1)) + 0.5);
shift++;
}
char *dec_point = strchr(buf, '.');
int digits_after_decimal = 0;
if (dec_point) {
digits_after_decimal = (int)strlen(dec_point + 1);
memmove(dec_point, dec_point + 1, strlen(dec_point));
int exp_out = -shift;
/* Strip trailing zeros */
while (coeff != 0 && coeff % 10 == 0) {
coeff /= 10;
exp_out++;
}
long long coeff_ll = atoll(buf);
*coefficient = (long)coeff_ll;
*exponent = exp_from_sci - digits_after_decimal;
*coefficient = (long)(coeff * sign);
*exponent = (long)exp_out;
}
static void nota_write_float_buf(NotaBuffer *nb, double d)

View File

@@ -11292,38 +11292,41 @@ static int nota_get_arr_len (JSContext *ctx, JSValue arr) {
return (int)len;
}
typedef struct NotaVisitedNode {
JSGCRef ref;
struct NotaVisitedNode *next;
} NotaVisitedNode;
typedef struct NotaEncodeContext {
JSContext *ctx;
JSGCRef *visitedStack_ref; /* pointer to GC-rooted ref */
NotaVisitedNode *visited_list;
NotaBuffer nb;
int cycle;
JSGCRef *replacer_ref; /* pointer to GC-rooted ref */
} NotaEncodeContext;
static void nota_stack_push (NotaEncodeContext *enc, JSValueConst val) {
JSContext *ctx = enc->ctx;
int len = nota_get_arr_len (ctx, enc->visitedStack_ref->val);
JS_SetPropertyNumber (ctx, enc->visitedStack_ref->val, len, JS_DupValue (ctx, val));
NotaVisitedNode *node = (NotaVisitedNode *)sys_malloc (sizeof (NotaVisitedNode));
JS_PushGCRef (enc->ctx, &node->ref);
node->ref.val = JS_DupValue (enc->ctx, val);
node->next = enc->visited_list;
enc->visited_list = node;
}
static void nota_stack_pop (NotaEncodeContext *enc) {
JSContext *ctx = enc->ctx;
int len = nota_get_arr_len (ctx, enc->visitedStack_ref->val);
JS_SetPropertyStr (ctx, enc->visitedStack_ref->val, "length", JS_NewUint32 (ctx, len - 1));
NotaVisitedNode *node = enc->visited_list;
enc->visited_list = node->next;
JS_FreeValue (enc->ctx, node->ref.val);
JS_PopGCRef (enc->ctx, &node->ref);
sys_free (node);
}
static int nota_stack_has (NotaEncodeContext *enc, JSValueConst val) {
JSContext *ctx = enc->ctx;
int len = nota_get_arr_len (ctx, enc->visitedStack_ref->val);
for (int i = 0; i < len; i++) {
JSValue elem = JS_GetPropertyNumber (ctx, enc->visitedStack_ref->val, i);
if (mist_is_gc_object (elem) && mist_is_gc_object (val)) {
if (JS_StrictEq (ctx, elem, val)) {
JS_FreeValue (ctx, elem);
return 1;
}
}
JS_FreeValue (ctx, elem);
NotaVisitedNode *node = enc->visited_list;
while (node) {
if (JS_StrictEq (enc->ctx, node->ref.val, val))
return 1;
node = node->next;
}
return 0;
}
@@ -11465,6 +11468,13 @@ static void nota_encode_value (NotaEncodeContext *enc, JSValueConst val, JSValue
nota_write_sym (&enc->nb, NOTA_NULL);
break;
case JS_TAG_PTR: {
if (JS_IsText (replaced_ref.val)) {
const char *str = JS_ToCString (ctx, replaced_ref.val);
nota_write_text (&enc->nb, str);
JS_FreeCString (ctx, str);
break;
}
if (js_is_blob (ctx, replaced_ref.val)) {
size_t buf_len;
void *buf_data = js_get_blob_data (ctx, &buf_len, replaced_ref.val);
@@ -11576,16 +11586,14 @@ static void nota_encode_value (NotaEncodeContext *enc, JSValueConst val, JSValue
}
void *value2nota (JSContext *ctx, JSValue v) {
JSGCRef val_ref, stack_ref, key_ref;
JSGCRef val_ref, key_ref;
JS_PushGCRef (ctx, &val_ref);
JS_PushGCRef (ctx, &stack_ref);
JS_PushGCRef (ctx, &key_ref);
val_ref.val = v;
NotaEncodeContext enc_s, *enc = &enc_s;
enc->ctx = ctx;
stack_ref.val = JS_NewArray (ctx);
enc->visitedStack_ref = &stack_ref;
enc->visited_list = NULL;
enc->cycle = 0;
enc->replacer_ref = NULL;
@@ -11595,14 +11603,12 @@ void *value2nota (JSContext *ctx, JSValue v) {
if (enc->cycle) {
JS_PopGCRef (ctx, &key_ref);
JS_PopGCRef (ctx, &stack_ref);
JS_PopGCRef (ctx, &val_ref);
nota_buffer_free (&enc->nb);
return NULL;
}
JS_PopGCRef (ctx, &key_ref);
JS_PopGCRef (ctx, &stack_ref);
JS_PopGCRef (ctx, &val_ref);
void *data_ptr = enc->nb.data;
enc->nb.data = NULL;
@@ -11630,18 +11636,16 @@ JSValue nota2value (JSContext *js, void *nota) {
static JSValue js_nota_encode (JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
if (argc < 1) return JS_ThrowTypeError (ctx, "nota.encode requires at least 1 argument");
JSGCRef val_ref, stack_ref, replacer_ref, key_ref;
JSGCRef val_ref, replacer_ref, key_ref;
JS_PushGCRef (ctx, &val_ref);
JS_PushGCRef (ctx, &stack_ref);
JS_PushGCRef (ctx, &replacer_ref);
JS_PushGCRef (ctx, &key_ref);
val_ref.val = argv[0];
stack_ref.val = JS_NewArray (ctx);
replacer_ref.val = (argc > 1 && JS_IsFunction (argv[1])) ? argv[1] : JS_NULL;
NotaEncodeContext enc_s, *enc = &enc_s;
enc->ctx = ctx;
enc->visitedStack_ref = &stack_ref;
enc->visited_list = NULL;
enc->cycle = 0;
enc->replacer_ref = &replacer_ref;
@@ -11662,7 +11666,6 @@ static JSValue js_nota_encode (JSContext *ctx, JSValueConst this_val, int argc,
JS_PopGCRef (ctx, &key_ref);
JS_PopGCRef (ctx, &replacer_ref);
JS_PopGCRef (ctx, &stack_ref);
JS_PopGCRef (ctx, &val_ref);
return ret;
}

View File

@@ -429,11 +429,15 @@ char *wota_read_text_len(size_t *byte_len, char **text_utf8, char *wota)
}
/* Copy bytes from the packed 64-bit words */
for (long long i = 0; i < nwords; i++) {
uint64_t wval = data_words[i];
for (int j = 0; j < 8 && (i * 8 + j) < (long long)nbytes; j++) {
out[i * 8 + j] = (char)((wval >> (56 - j * 8)) & 0xff);
}
size_t full_words = nbytes / 8;
size_t remainder = nbytes % 8;
for (size_t i = 0; i < full_words; i++) {
uint64_t wval = wota_bswap64(data_words[i]);
memcpy(out + i * 8, &wval, 8);
}
if (remainder > 0) {
uint64_t wval = wota_bswap64(data_words[full_words]);
memcpy(out + full_words * 8, &wval, remainder);
}
out[nbytes] = '\0';
@@ -609,14 +613,18 @@ void wota_write_text_len(WotaBuffer *wb, const char *utf8, size_t nbytes)
}
uint64_t *blocks = wota_buffer_alloc(wb, nwords);
memset(blocks, 0, nwords * sizeof(uint64_t));
for (size_t i = 0; i < nwords; i++) {
size_t full_words = nbytes / 8;
size_t remainder = nbytes % 8;
for (size_t i = 0; i < full_words; i++) {
uint64_t wval;
memcpy(&wval, utf8 + i * 8, 8);
blocks[i] = wota_bswap64(wval);
}
if (remainder > 0) {
uint64_t wval = 0;
for (int j = 0; j < 8 && (i * 8 + j) < nbytes; j++) {
wval |= ((uint64_t)(unsigned char)utf8[i * 8 + j]) << (56 - j * 8);
}
blocks[i] = wval;
memcpy(&wval, utf8 + full_words * 8, remainder);
blocks[full_words] = wota_bswap64(wval);
}
}