diff --git a/benches/encoders.cm b/benches/encoders.cm new file mode 100644 index 00000000..4c53ad48 --- /dev/null +++ b/benches/encoders.cm @@ -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 + } +} diff --git a/source/nota.h b/source/nota.h index 23508a2d..e969f459 100755 --- a/source/nota.h +++ b/source/nota.h @@ -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) diff --git a/source/runtime.c b/source/runtime.c index c2430c88..26e42421 100644 --- a/source/runtime.c +++ b/source/runtime.c @@ -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; } diff --git a/source/wota.h b/source/wota.h index 79f09422..454806da 100644 --- a/source/wota.h +++ b/source/wota.h @@ -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); } }