diff options
| author | eunhhu <[email protected]> | 2026-10-18 16:44:17 +0100 |
|---|---|---|
| committer | eunhhu <[email protected]> | 2026-10-18 16:44:17 +0100 |
| commit | 241016b0fc3139414089619f0ef8cb7237722c6c (patch) | |
| tree | b782ed50757ee910278c8d1129dcfc7ded48d527 /research | |
| parent | 433f56c29b26b83cd2991b4a834b427fae3e75d4 (diff) | |
| download | pure-241016b0fc3139414089619f0ef8cb7237722c6c.tar.gz pure-241016b0fc3139414089619f0ef8cb7237722c6c.zip | |
Research 140, 027's stage 2c measured: JSON read into a tree by recursive descent (aup's examples/json_tree.aup: value(d, i) -> Json, number giving the value and where it ends, each call's own let's lists, string's escapes joined by char(u), a depth-first walk passing items as values) against Python's reading walked so: JSONTestSuite's 95 y_ taken alike, no n_ taken, 2b's 6 i_ taken (a lone surrogate refused by char as encoding); 100,000 and 50,000 deep refused as memory (the frames fill the event's room, 0.2 s), 30,000 deep taken (past Python's json); 300 random documents alike; in make check (module_specs' digest now depth first, three paths); 142 lines against the same design's 105 in Python (json_rec.py; the bar missed: 24 lines of declarations, 23 of braces), 46 times cJSON's time and 1.6 times plain Python's (600 ns a byte, 248 Pure instructions; 71% in the steps, about 10% in calls, 66,000 calls a document, most of them small block fns like skip); characters against Python's ord in aup's check; probes: p5-chars compiles, p4-rec (a view calling itself) refused; instruction counts: the workloads' modules the same, batches within 0.001% of 2b (BFS -0.21%), the executor alone -0.001% to +0.30%, the bank's queries -0.30% to +0.11% (four runs each; bar 1%); json_rec_time.c (cJSON, a depth-first digest compared first); CHECKPOINT's direction row through 2c
Diffstat (limited to 'research')
6 files changed, 312 insertions, 0 deletions
diff --git a/research/experiments/040-against-c/2026-10-18-counts-s27.jsonl b/research/experiments/040-against-c/2026-10-18-counts-s27.jsonl new file mode 100644 index 0000000..6189828 --- /dev/null +++ b/research/experiments/040-against-c/2026-10-18-counts-s27.jsonl @@ -0,0 +1,4 @@ +{"serve": "pure-serve", "per_branch": {"instructions": 27104679, "cycles": 11890487}, "in_credit": {"instructions": 3517992, "cycles": 1599796}, "large": {"instructions": 1421141, "cycles": 635549}, "ranks": {"instructions": 24461843, "cycles": 14883419}, "none": {"instructions": 617594, "cycles": 206190}, "field": {"instructions": 8999054, "cycles": 3366751}, "found": {"instructions": 14151357, "cycles": 10583883}} +{"serve": "pure-serve", "per_branch": {"instructions": 27000469, "cycles": 11861597}, "in_credit": {"instructions": 3494299, "cycles": 1586220}, "large": {"instructions": 1421606, "cycles": 612299}, "ranks": {"instructions": 24408016, "cycles": 14881533}, "none": {"instructions": 617123, "cycles": 194839}, "field": {"instructions": 9008753, "cycles": 3313430}, "found": {"instructions": 14162216, "cycles": 10579828}} +{"serve": "pure-serve", "per_branch": {"instructions": 27179606, "cycles": 11895677}, "in_credit": {"instructions": 3534978, "cycles": 1593510}, "large": {"instructions": 1421029, "cycles": 615442}, "ranks": {"instructions": 24504036, "cycles": 14990092}, "none": {"instructions": 617586, "cycles": 195867}, "field": {"instructions": 8999045, "cycles": 3337164}, "found": {"instructions": 14147990, "cycles": 10807080}} +{"serve": "pure-serve", "per_branch": {"instructions": 27087988, "cycles": 11880543}, "in_credit": {"instructions": 3514114, "cycles": 1652760}, "large": {"instructions": 1421534, "cycles": 679535}, "ranks": {"instructions": 24458239, "cycles": 15232390}, "none": {"instructions": 617115, "cycles": 194019}, "field": {"instructions": 9008642, "cycles": 3371309}, "found": {"instructions": 14160887, "cycles": 10559598}} diff --git a/research/experiments/040-against-c/2026-10-18-counts-s27b.jsonl b/research/experiments/040-against-c/2026-10-18-counts-s27b.jsonl new file mode 100644 index 0000000..d230430 --- /dev/null +++ b/research/experiments/040-against-c/2026-10-18-counts-s27b.jsonl @@ -0,0 +1,4 @@ +{"serve": "pure-serve", "per_branch": {"instructions": 26965513, "cycles": 12329820}, "in_credit": {"instructions": 3486678, "cycles": 2301853}, "large": {"instructions": 1421213, "cycles": 764526}, "ranks": {"instructions": 24384330, "cycles": 14965408}, "none": {"instructions": 617594, "cycles": 198786}, "field": {"instructions": 8999053, "cycles": 3340870}, "found": {"instructions": 14153477, "cycles": 10932153}} +{"serve": "pure-serve", "per_branch": {"instructions": 27165700, "cycles": 11943061}, "in_credit": {"instructions": 3531402, "cycles": 1625620}, "large": {"instructions": 1421494, "cycles": 606837}, "ranks": {"instructions": 24499209, "cycles": 14876346}, "none": {"instructions": 617115, "cycles": 193396}, "field": {"instructions": 9008642, "cycles": 3349618}, "found": {"instructions": 14160543, "cycles": 10702223}} +{"serve": "pure-serve", "per_branch": {"instructions": 27199887, "cycles": 12121684}, "in_credit": {"instructions": 3539333, "cycles": 1628967}, "large": {"instructions": 1421149, "cycles": 608625}, "ranks": {"instructions": 24513416, "cycles": 15034269}, "none": {"instructions": 617594, "cycles": 195387}, "field": {"instructions": 8999053, "cycles": 3333569}, "found": {"instructions": 14151773, "cycles": 10702538}} +{"serve": "pure-serve", "per_branch": {"instructions": 27106101, "cycles": 11890926}, "in_credit": {"instructions": 3518026, "cycles": 1605106}, "large": {"instructions": 1421542, "cycles": 619029}, "ranks": {"instructions": 24466175, "cycles": 14887083}, "none": {"instructions": 617115, "cycles": 192958}, "field": {"instructions": 9008642, "cycles": 3345733}, "found": {"instructions": 14162615, "cycles": 10752104}} diff --git a/research/experiments/041-batches/2026-10-18-counts.jsonl b/research/experiments/041-batches/2026-10-18-counts.jsonl new file mode 100644 index 0000000..d1f5c4d --- /dev/null +++ b/research/experiments/041-batches/2026-10-18-counts.jsonl @@ -0,0 +1,20 @@ +{"version":"b27","way":"batches","work":"matmul","instructions":713395360,"cycles":230040058} +{"version":"b27","way":"rounds","work":"matmul","instructions":21265006173,"cycles":7306797350} +{"version":"b27","way":"batches","work":"knapsack","instructions":348632565,"cycles":105675668} +{"version":"b27","way":"rounds","work":"knapsack","instructions":21881484247,"cycles":8126458330} +{"version":"b27","way":"batches","work":"roots","instructions":157304235,"cycles":109481210} +{"version":"b27","way":"rounds","work":"roots","instructions":2719816869,"cycles":1107162486} +{"version":"b27","way":"batches","work":"join","instructions":1783718427,"cycles":1025538655} +{"version":"b27","way":"rounds","work":"join","instructions":7669671940,"cycles":3506384789} +{"version":"b27","way":"batches","work":"bfs","instructions":3364523250,"cycles":1646034641} +{"version":"b27","way":"rounds","work":"bfs","instructions":4726341201,"cycles":2095450104} +{"version":"b26","way":"batches","work":"matmul","instructions":713390693,"cycles":229445239} +{"version":"b26","way":"rounds","work":"matmul","instructions":21265242319,"cycles":7078307485} +{"version":"b26","way":"batches","work":"knapsack","instructions":348629421,"cycles":105752256} +{"version":"b26","way":"rounds","work":"knapsack","instructions":21816293868,"cycles":7885750723} +{"version":"b26","way":"batches","work":"roots","instructions":157302159,"cycles":109456602} +{"version":"b26","way":"rounds","work":"roots","instructions":2719815079,"cycles":1040127483} +{"version":"b26","way":"batches","work":"join","instructions":1783717810,"cycles":1064908324} +{"version":"b26","way":"rounds","work":"join","instructions":7659168467,"cycles":3373811804} +{"version":"b26","way":"batches","work":"bfs","instructions":3371506688,"cycles":1643771467} +{"version":"b26","way":"rounds","work":"bfs","instructions":4712039680,"cycles":2075728320} diff --git a/research/experiments/044-specs/2026-10-18-stage2c.jsonl b/research/experiments/044-specs/2026-10-18-stage2c.jsonl new file mode 100644 index 0000000..99972d6 --- /dev/null +++ b/research/experiments/044-specs/2026-10-18-stage2c.jsonl @@ -0,0 +1,6 @@ +{"size":103781,"jit":"0","batch":"","c_ns_per_byte":13.050,"pure_ns_per_byte":599.753,"ratio":46.0,"instructions":25733137,"instructions_per_byte":248.0} +{"size":103781,"python_ns_per_byte":367.8,"python_json_module_ns_per_byte":54.0} +{"size":103781,"jit":"0","batch":"","c_ns_per_byte":13.814,"pure_ns_per_byte":600.877,"ratio":43.5,"instructions":25733137,"instructions_per_byte":248.0} +{"size":103781,"python_ns_per_byte":365.5,"python_json_module_ns_per_byte":53.8} +{"size":103781,"jit":"0","batch":"","c_ns_per_byte":13.141,"pure_ns_per_byte":598.454,"ratio":45.5,"instructions":25733137,"instructions_per_byte":248.0} +{"size":103781,"python_ns_per_byte":369.3,"python_json_module_ns_per_byte":54.8} diff --git a/research/experiments/044-specs/json_rec.py b/research/experiments/044-specs/json_rec.py new file mode 100644 index 0000000..c7951e5 --- /dev/null +++ b/research/experiments/044-specs/json_rec.py @@ -0,0 +1,159 @@ +# A JSON text read into a tree by recursive descent and walked depth first for a digest: the design of aup's +# examples/json_tree.aup in 027's stage 2c, in plain Python (the reference 140 counts its lines against; the tree is +# Python's own values, an object kept as pairs). A text that is no JSON raises (ValueError, or IndexError past its +# end), as the event fails. +import re + + +class Pairs(list): + pass + + +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 + + +def require(ok): + if not ok: + raise ValueError + + +def code(d, i): + return int(d[i:i + 4], 16) + + +def escape(d, i): + return d[i] == 0x5c and d[i + 1] == 0x75 and all(hexdigit(d[k]) for k in range(i + 2, i + 6)) + + +def skip(d, i): + while i < len(d) and space(d[i]): + i += 1 + return i + + +def string(d, i): + require(d[i] == 0x22) + s, j = bytearray(), i + 1 + while d[j] != 0x22: + start = j + while d[j] != 0x22 and d[j] != 0x5c and d[j] >= 0x20: + j += 1 + s += d[start:j] + require(d[j] >= 0x20) + if d[j] == 0x5c: + e = d[j + 1] + if e == 0x75: + require(escape(d, j)) + u = code(d, j + 2) + j += 6 + if 0xd800 <= u < 0xdc00 and j + 5 < len(d) and escape(d, j) and 0xdc00 <= code(d, j + 2) < 0xe000: + u = 0x10000 + ((u - 0xd800) << 10 | code(d, j + 2) - 0xdc00) + j += 6 + s += chr(u).encode() # a lone surrogate raises + else: + require(e in b'"\\/bfnrt') + s += {0x62: b'\b', 0x66: b'\f', 0x6e: b'\n', 0x72: b'\r', 0x74: b'\t'}.get(e, bytes([e])) + j += 2 + return s.decode(), j + 1 + + +def number(t): + require(re.fullmatch(r'-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?', t)) + x = float(t) + require(abs(x) != float('inf')) + return x + + +def value(d, i): + j = skip(d, i) + b = d[j] + if b in (0x5b, 0x7b): + items = [] if b == 0x5b else Pairs() + j = skip(d, j + 1) + if d[j] != b + 2: + while True: + if b == 0x5b: + v, k = value(d, j) + items.append(v) + else: + name, k = string(d, j) + k = skip(d, k) + require(d[k] == 0x3a) + v, k = value(d, k + 1) + items.append((name, v)) + j = skip(d, k) + if d[j] != 0x2c: + break + j = skip(d, j + 1) + require(d[j] == b + 2) + return items, j + 1 + if b == 0x22: + return string(d, j) + if b == 0x2d or digit(b): + start, j = j, j + 1 + while j < len(d) and (digit(d[j]) or d[j] in b'.eE+-'): + j += 1 + return number(d[start:j].decode()), j + for word, v in ((b'true', True), (b'false', False), (b'null', None)): + if d[j:j + len(word)] == word: + return v, j + len(word) + raise ValueError + + +def walk(x, d): + 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()) + elif isinstance(x, Pairs): + d['objects'] += 1 + for k, v in x: + d['key_bytes'] += len(k.encode()) + walk(v, d) + else: + d['arrays'] += 1 + for y in x: + walk(y, d) + + +def check(data): + data.decode() # UTF-8, as text(check.data) is + root, end = value(data, 0) + require(skip(data, end) == len(data)) + 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) + walk(root, d) + return d + + +if __name__ == '__main__': + import json, os, sys, time + sys.setrecursionlimit(100000) + suite = sys.argv[1] + for name in sorted(os.listdir(suite)): + try: + got = check(open(os.path.join(suite, name), 'rb').read()) + except (ValueError, IndexError, RecursionError): + got = None + assert not name.startswith('y_') or got is not None, name + assert not name.startswith('n_') or got 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() + check(big) + runs.append((time.perf_counter() - start) * 1e9 / len(big)) + start = time.perf_counter() + 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) + walk(json.loads(big, object_pairs_hook=Pairs, parse_int=float), d) + 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_rec_time.c b/research/experiments/044-specs/json_rec_time.c new file mode 100644 index 0000000..03c02c4 --- /dev/null +++ b/research/experiments/044-specs/json_rec_time.c @@ -0,0 +1,119 @@ +/* Research 140, design 027 stage 2c: aup's examples/json_tree.aup (run by + * Pure: a JSON text read into a tree by recursive descent, then a digest of + * the tree, depth first) 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_rec_time.c ../../../build/libpure.a -l:libcjson.so.1 -lm -pthread -o json_rec_time + * ./json_rec_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 <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> + +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: depth first, as json_tree.aup. */ +typedef struct { double count[7], sum, text_bytes, key_bytes; } Digest; +static void walk(const cJSON *x, Digest *d) { + 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 (const cJSON *y = x->child; y; y = y->next) walk(y, d); break; + default: d->count[6]++; for (const cJSON *y = x->child; y; y = y->next) d->key_bytes += (double)strlen(y->string), walk(y, d); + } +} +static Digest digest(const char *text, cJSON **queue) { + (void)queue; + Digest d = {{0}, 0, 0, 0}; + cJSON *root = cJSON_Parse(text); + walk(root, &d); + 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; +} |
