From 010fa06adc3b05e3e164309ee34e200f5d858fa2 Mon Sep 17 00:00:00 2001 From: eunhhu Date: Tue, 13 Oct 2026 07:40:30 +0100 Subject: Research 140, 027's stage 2b measured: JSON read into a tree (aup's examples/json_tree.aup: a variant, lists in records, match, appends into a field's list, number(t)) and walked for a digest, against Python's reading: JSONTestSuite's 95 y_ taken alike, no n_ taken, 6 i_ taken; 300 random nested documents alike; in make check (module_specs, the suite and 100 random documents, three paths); 167 lines against the same design's 133 in Python (the bar missed: the type declarations), 40 times cJSON's time and 1.3 times plain Python's (556 ns a byte, 525 instructions); json_tree.py, json_tree_time.c (cJSON linked, digests compared first) --- .../experiments/044-specs/2026-10-13-stage2b.jsonl | 6 + research/experiments/044-specs/json_tree.py | 164 +++++++++++++++++++++ research/experiments/044-specs/json_tree_time.c | 119 +++++++++++++++ 3 files changed, 289 insertions(+) create mode 100644 research/experiments/044-specs/2026-10-13-stage2b.jsonl create mode 100644 research/experiments/044-specs/json_tree.py create mode 100644 research/experiments/044-specs/json_tree_time.c (limited to 'research/experiments') diff --git a/research/experiments/044-specs/2026-10-13-stage2b.jsonl b/research/experiments/044-specs/2026-10-13-stage2b.jsonl new file mode 100644 index 0000000..27de90b --- /dev/null +++ b/research/experiments/044-specs/2026-10-13-stage2b.jsonl @@ -0,0 +1,6 @@ +{"size":103781,"jit":"0","batch":"0","c_ns_per_byte":14.717,"pure_ns_per_byte":554.145,"ratio":37.7,"instructions":54505171,"instructions_per_byte":525.2} +{"size":103781,"jit":"0","batch":"","c_ns_per_byte":13.756,"pure_ns_per_byte":555.296,"ratio":40.4,"instructions":54505171,"instructions_per_byte":525.2} +{"size":103781,"jit":"all","batch":"","c_ns_per_byte":14.223,"pure_ns_per_byte":555.444,"ratio":39.1,"instructions":54505171,"instructions_per_byte":525.2} +{"size":103781,"jit":"","batch":"","c_ns_per_byte":13.899,"pure_ns_per_byte":555.628,"ratio":40.0,"instructions":54505171,"instructions_per_byte":525.2} +{"size":103781,"python_ns_per_byte":426.5,"python_json_module_ns_per_byte":62.3} +{"json_tree_aup_lines": 167, "aup_brace_lines": 20, "python_lines": 133, "jsontestsuite": {"y_taken": 95, "n_taken": 0, "i_taken": 6}, "random_documents": 300, "wrong": 0} diff --git a/research/experiments/044-specs/json_tree.py b/research/experiments/044-specs/json_tree.py new file mode 100644 index 0000000..8807cdc --- /dev/null +++ b/research/experiments/044-specs/json_tree.py @@ -0,0 +1,164 @@ +# A JSON text read into a tree and walked breadth first for a digest: the design of aup's examples/json_tree.aup in +# plain Python (the reference 140 counts its lines against; the tree is Python's own values, a dict kept as pairs). +def space(b): + return b in (0x20, 0x09, 0x0a, 0x0d) + + +def digit(b): + return 0x30 <= b <= 0x39 + + +def hexdigit(b): + return digit(b) or 0x61 <= b <= 0x66 or 0x41 <= b <= 0x46 + + +class Pairs(list): + pass + + +def read(d): + try: + d.decode('utf-8') + except UnicodeDecodeError: + return None + n, i, ok, mode, root, open_, v = len(d), 0, True, 0, [], [], [] + while ok: + while i < n and space(d[i]): + i += 1 + if i == n: + ok = mode == 1 and not open_ + break + b = d[i] + if mode == 1: + if not open_: + ok = False + elif b == 0x2c: + mode = 2 if open_[-1]['object'] else 0 + elif b == 0x5d and not open_[-1]['object']: + v.append(open_[-1]['items']) + elif b == 0x7d and open_[-1]['object']: + v.append(Pairs(open_[-1]['members'])) + else: + ok = False + if v: + open_.pop() + i += 1 + elif mode == 3 and b == 0x7d or mode == 4 and b == 0x5d: + v.append(Pairs(open_[-1]['members']) if b == 0x7d else open_[-1]['items']) + open_.pop() + i += 1 + elif mode in (2, 3): + ok = b == 0x22 + mode = 5 + elif b in (0x5b, 0x7b): + open_.append({'items': [], 'members': [], 'name': '', 'object': b == 0x7b}) + mode = 4 if b == 0x5b else 3 + i += 1 + elif b == 0x22 or mode == 5: + i += 1 + s = bytearray() + while ok and i < n and d[i] != 0x22: + start = i + while i < n and d[i] != 0x22 and d[i] != 0x5c and d[i] >= 0x20: + i += 1 + s += d[start:i] + if i < n and d[i] < 0x20: + ok = False + elif i + 1 < n and d[i] == 0x5c: + e = d[i + 1] + if e == 0x75: + ok = i + 5 < n and all(hexdigit(x) for x in d[i + 2:i + 6]) + u = int(d[i + 2:i + 6], 16) if ok else 0 + i += 6 + if 0xd800 <= u < 0xdc00 and i + 5 < n and d[i] == 0x5c and d[i + 1] == 0x75 and all(hexdigit(x) for x in d[i + 2:i + 6]): + low = int(d[i + 2:i + 6], 16) + if 0xdc00 <= low < 0xe000: + u = 0x10000 + ((u - 0xd800) << 10 | low - 0xdc00) + i += 6 + s += chr(u).encode('utf-8', 'surrogatepass') + else: + ok = e in b'"\\/bfnrt' + s += {0x62: b'\b', 0x66: b'\f', 0x6e: b'\n', 0x72: b'\r', 0x74: b'\t'}.get(e, bytes([e])) + i += 2 + elif i < n and d[i] == 0x5c: + ok = False + ok = ok and i < n + i += 1 + try: + t = s.decode('utf-8') + except UnicodeDecodeError: + return None + if mode == 5: + open_[-1]['name'] = t + while i < n and space(d[i]): + i += 1 + ok = ok and i < n and d[i] == 0x3a + i += 1 + mode = 0 + else: + v.append(t) + elif b == 0x2d or digit(b): + start = i + if b == 0x2d: + i += 1 + while i < n and (digit(d[i]) or d[i] in b'.eE+-'): + i += 1 + text = d[start:i].decode() + import re + if not re.fullmatch(r'-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?', text) or float(text) in (float('inf'), -float('inf')): + return None + v.append(float(text)) + else: + word = b'true' if b == 0x74 else b'false' if b == 0x66 else b'null' + ok = d[i:i + len(word)] == word + i += len(word) + if ok: + v.append({b'true': True, b'false': False, b'null': None}[word]) + if ok and v: + if not open_: + ok = not root + root.append(v[-1]) + elif open_[-1]['object']: + open_[-1]['members'].append((open_[-1]['name'], v[-1])) + else: + open_[-1]['items'].append(v[-1]) + v.pop() + mode = 1 + return root if ok else None + + +def digest(tree): + d = dict(nulls=0, trues=0, falses=0, numbers=0, strings=0, arrays=0, objects=0, sum=0.0, text_bytes=0, key_bytes=0) + q, at = [tree], 0 + while at < len(q): + x = q[at] + if x is None: d['nulls'] += 1 + elif x is True: d['trues'] += 1 + elif x is False: d['falses'] += 1 + elif isinstance(x, float): d['numbers'] += 1; d['sum'] += x + elif isinstance(x, str): d['strings'] += 1; d['text_bytes'] += len(x.encode('utf-8')) + elif isinstance(x, Pairs): + d['objects'] += 1 + for k, y in x: d['key_bytes'] += len(k.encode('utf-8')); q.append(y) + else: d['arrays'] += 1; q.extend(x) + at += 1 + return d + + +if __name__ == '__main__': + import json, os, sys, time + suite = sys.argv[1] + for name in sorted(os.listdir(suite)): + tree = read(open(os.path.join(suite, name), 'rb').read()) + assert not name.startswith('y_') or tree is not None, name + assert not name.startswith('n_') or tree is None, name + big = b'[' + b','.join(b'{"key%d": [1.5e3, true, null, "text \\u00e9 %d"]}' % (k, k) for k in range(2000)) + b']' + runs, loads = [], [] + for _ in range(7): + start = time.perf_counter() + digest(read(big)[0]) + runs.append((time.perf_counter() - start) * 1e9 / len(big)) + start = time.perf_counter() + digest(json.loads(big, object_pairs_hook=Pairs)) + loads.append((time.perf_counter() - start) * 1e9 / len(big)) + print('{"size":%d,"python_ns_per_byte":%.1f,"python_json_module_ns_per_byte":%.1f}' % (len(big), sorted(runs)[3], sorted(loads)[3])) diff --git a/research/experiments/044-specs/json_tree_time.c b/research/experiments/044-specs/json_tree_time.c new file mode 100644 index 0000000..1cac21a --- /dev/null +++ b/research/experiments/044-specs/json_tree_time.c @@ -0,0 +1,119 @@ +/* Research 140, design 027 stage 2b: aup's examples/json_tree.aup (run by + * Pure: a JSON text read into a tree, then a digest of the tree) against C: + * cJSON (the system's libcjson.so.1, its few declarations written here) + * reading the same document into its tree, and the same digest. Both digests + * must be equal; then each is timed (the median of seven runs of rounds). + * + * cc -O2 -std=c17 -I../../../core json_tree_time.c ../../../build/libpure.a -l:libcjson.so.1 -lm -pthread -o json_tree_time + * ./json_tree_time json_tree.pmod ROUNDS + * + * The document is json_time.c's: 2,000 objects of a key and an array of a + * number, true, null and a string with an escape. One JSON line. */ +#define _POSIX_C_SOURCE 200809L +#include "module.h" +#include +#include +#include +#include + +typedef struct cJSON { + struct cJSON *next, *prev, *child; + int type; + char *valuestring; + int valueint; + double valuedouble; + char *string; +} cJSON; +cJSON *cJSON_Parse(const char *value); +void cJSON_Delete(cJSON *item); +enum { FALSE_ = 1, TRUE_ = 2, NULL_ = 4, NUMBER = 8, STRING = 16, ARRAY = 32, OBJECT = 64 }; + +/* nulls, trues, falses, numbers, strings, arrays, objects, sum, text bytes, key bytes: breadth first, as json_tree.aup. */ +typedef struct { double count[7], sum, text_bytes, key_bytes; } Digest; +static Digest digest(const char *text, cJSON **queue) { + Digest d = {{0}, 0, 0, 0}; + cJSON *root = cJSON_Parse(text); + size_t at = 0, end = 0; + queue[end++] = root; + while (at < end) { + cJSON *x = queue[at++]; + switch (x->type & 0xFF) { + case NULL_: d.count[0]++; break; + case TRUE_: d.count[1]++; break; + case FALSE_: d.count[2]++; break; + case NUMBER: d.count[3]++, d.sum += x->valuedouble; break; + case STRING: d.count[4]++, d.text_bytes += (double)strlen(x->valuestring); break; + case ARRAY: d.count[5]++; for (cJSON *y = x->child; y; y = y->next) queue[end++] = y; break; + default: d.count[6]++; for (cJSON *y = x->child; y; y = y->next) d.key_bytes += (double)strlen(y->string), queue[end++] = y; + } + } + cJSON_Delete(root); + return d; +} + +static double now_ns(void) { + struct timespec t; + clock_gettime(CLOCK_MONOTONIC, &t); + return (double)t.tv_sec * 1e9 + (double)t.tv_nsec; +} + +static int by_value(const void *a, const void *b) { + double x = *(const double *)a, y = *(const double *)b; + return (x > y) - (x < y); +} + +int main(int argc, char **argv) { + if (argc != 3) { fprintf(stderr, "usage: %s MODULE ROUNDS\n", argv[0]); return 2; } + int rounds = atoi(argv[2]); + static char doc[1 << 18]; + static cJSON *queue[1 << 16]; + size_t n = 0; + doc[n++] = '['; + for (int k = 0; k < 2000; ++k) + n += (size_t)sprintf(doc + n, "%s{\"key%d\": [1.5e3, true, null, \"text \\u00e9 %d\"]}", k ? "," : "", k, k); + doc[n++] = ']'; + doc[n] = 0; + PureModule module; + char error[160]; + if (pure_module_read(argv[1], &module, error, sizeof(error)) != PURE_OK) { fprintf(stderr, "%s\n", error); return 1; } + PureApp app; + if (pure_app_start(&app, &module, (PureData){0}) != PURE_OK) return 1; + int check = pure_module_input(&module, "check"); + if (pure_app_message(&app, (uint32_t)check, NULL, -1, (PureSpan){(const unsigned char *)doc, n}) != PURE_OK) { + fprintf(stderr, "event failed\n"); + return 1; + } + uint64_t work = app.work; + Digest c = digest(doc, queue); + /* The states after valid: nulls, trues, falses, numbers, strings, arrays, objects, sum, text_bytes, key_bytes. */ + const char *names[] = {"nulls", "trues", "falses", "numbers", "strings", "arrays", "objects", "sum", "text_bytes", "key_bytes"}; + double want[] = {c.count[0], c.count[1], c.count[2], c.count[3], c.count[4], c.count[5], c.count[6], c.sum, c.text_bytes, c.key_bytes}; + for (unsigned k = 0; k < 10; ++k) { + int state = -1; + for (uint32_t s = 0; s < module.states; ++s) if (!strcmp(module.state_names[s], names[k])) state = (int)s; + if (state < 0 || app.values[state].num != want[k]) { + fprintf(stderr, "%s: %g against %g\n", names[k], state < 0 ? -1 : app.values[state].num, want[k]); + return 1; + } + } + double c_runs[7], pure_runs[7]; + volatile double sink = 0; + for (int r = 0; r < 7; ++r) { + double start = now_ns(); + for (int i = 0; i < rounds; ++i) sink += digest(doc, queue).sum; + c_runs[r] = (now_ns() - start) / rounds; + start = now_ns(); + for (int i = 0; i < rounds; ++i) + if (pure_app_message(&app, (uint32_t)check, NULL, -1, (PureSpan){(const unsigned char *)doc, n}) != PURE_OK) return 1; + pure_runs[r] = (now_ns() - start) / rounds; + } + qsort(c_runs, 7, sizeof(double), by_value); + qsort(pure_runs, 7, sizeof(double), by_value); + const char *jit = getenv("PURE_JIT"), *batch = getenv("PURE_BATCH"); + printf("{\"size\":%zu,\"jit\":\"%s\",\"batch\":\"%s\",\"c_ns_per_byte\":%.3f,\"pure_ns_per_byte\":%.3f,\"ratio\":%.1f," + "\"instructions\":%llu,\"instructions_per_byte\":%.1f}\n", + n, jit ? jit : "", batch ? batch : "", c_runs[3] / (double)n, pure_runs[3] / (double)n, pure_runs[3] / c_runs[3], + (unsigned long long)work, (double)work / (double)n); + pure_app_stop(&app); + return 0; +} -- cgit v1.2.3