diff options
| author | eunhhu <[email protected]> | 2026-10-13 06:16:12 +0100 |
|---|---|---|
| committer | eunhhu <[email protected]> | 2026-10-13 06:16:12 +0100 |
| commit | e0276bfa9fd4bb418d0f82e12f09069629185863 (patch) | |
| tree | ac7e40c8fa5388ae7d3c97a6b27512d62e9fab36 /research | |
| parent | 8568c8732ca36673b073688bdf99cc25d6b8065a (diff) | |
| download | pure-e0276bfa9fd4bb418d0f82e12f09069629185863.tar.gz pure-e0276bfa9fd4bb418d0f82e12f09069629185863.zip | |
Research 140, 027's stage 2a measured: whether bytes are JSON (aup's examples/json.aup, 80 lines against the same design's 90 in Python): JSONTestSuite's 95 y_ cases taken and 188 n_ refused, 21 of the 35 i_ taken, the three paths alike, in make check (module_specs, the 316 small cases in tests/specs/json under their MIT license, the two large ones made and hashed); 116 times C's time and 1.2 times plain Python's (299 ns a byte, 339 instructions); && and || made to decide alone (writing it, i < n && d[i] read past the end) with AND and OR kept where the right side cannot fail, every other module byte for byte the same; SHA-256 again with formats and joined bytes, 42 lines and 159 times C
Diffstat (limited to 'research')
| -rw-r--r-- | research/experiments/044-specs/2026-10-13-stage2a.jsonl | 8 | ||||
| -rw-r--r-- | research/experiments/044-specs/json_check.py | 115 | ||||
| -rw-r--r-- | research/experiments/044-specs/json_time.c | 132 |
3 files changed, 255 insertions, 0 deletions
diff --git a/research/experiments/044-specs/2026-10-13-stage2a.jsonl b/research/experiments/044-specs/2026-10-13-stage2a.jsonl new file mode 100644 index 0000000..e536b54 --- /dev/null +++ b/research/experiments/044-specs/2026-10-13-stage2a.jsonl @@ -0,0 +1,8 @@ +{"size":103781,"jit":"0","batch":"0","c_ns_per_byte":2.567,"pure_ns_per_byte":298.133,"ratio":116.1,"instructions":35226511,"instructions_per_byte":339.4} +{"size":103781,"jit":"0","batch":"","c_ns_per_byte":2.567,"pure_ns_per_byte":298.362,"ratio":116.2,"instructions":35226511,"instructions_per_byte":339.4} +{"size":103781,"jit":"all","batch":"","c_ns_per_byte":2.569,"pure_ns_per_byte":298.453,"ratio":116.2,"instructions":35226511,"instructions_per_byte":339.4} +{"size":103781,"jit":"","batch":"","c_ns_per_byte":2.571,"pure_ns_per_byte":299.164,"ratio":116.4,"instructions":35226511,"instructions_per_byte":339.4} +{"size":103781,"python_ns_per_byte":247.0} +{"size":65536,"jit":"0","batch":"0","c_ns_per_byte":9.025,"pure_ns_per_byte":1437.304,"ratio":159.3,"instructions":16887501,"instructions_per_block":16476} +{"size":65536,"jit":"","batch":"","c_ns_per_byte":9.051,"pure_ns_per_byte":1439.504,"ratio":159.0,"instructions":16887501,"instructions_per_block":16476} +{"messages": 142, "right": {"batches": 142, "rounds": 142, "all": 142, "default": 142}, "aup_lines": 42, "aup_brace_lines": 5, "python_lines": 28} diff --git a/research/experiments/044-specs/json_check.py b/research/experiments/044-specs/json_check.py new file mode 100644 index 0000000..b5d62f0 --- /dev/null +++ b/research/experiments/044-specs/json_check.py @@ -0,0 +1,115 @@ +# Whether bytes are one JSON text (RFC 8259): the design of aup's examples/json.aup in plain Python, the reference +# 140 counts its lines against (read once from the start, a stack of the arrays and objects open). +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 valid(d): + try: + t = d.decode('utf-8') + except UnicodeDecodeError: + return False + n, i, mode, ok, open_ = len(d), 0, 0, True, [] # mode 0: a value, 1: after one, 2: a key, 3: a key or }, 4: a value or ] + while ok: + while i < n and space(d[i]): + i += 1 + if i == n: + return mode == 1 and not open_ + b = d[i] + if mode == 1: + if not open_: + ok = False + elif b == 0x2c: + mode = 0 if open_[-1] == 0x5b else 2 + elif b == open_[-1] + 2: + open_.pop() + else: + ok = False + i += 1 + elif mode == 3 and b == 0x7d or mode == 4 and b == 0x5d: + open_.pop() + mode = 1 + i += 1 + elif mode in (2, 3): + ok = b == 0x22 + mode = 5 + elif b in (0x5b, 0x7b): + open_.append(b) + mode = 4 if b == 0x5b else 3 + i += 1 + elif b == 0x22 or mode == 5: + i += 1 + while i < n and d[i] != 0x22 and ok: + if d[i] < 0x20: + ok = False + elif d[i] == 0x5c and i + 1 < n and d[i + 1] == 0x75: + ok = i + 5 < n and all(hexdigit(x) for x in d[i + 2:i + 6]) + i += 6 + elif d[i] == 0x5c: + ok = i + 1 < n and d[i + 1] in b'"\\/bfnrt' + i += 2 + else: + i += 1 + ok = ok and i < n + i += 1 + if mode == 5: + while i < n and space(d[i]): + i += 1 + ok = ok and i < n and d[i] == 0x3a + i += 1 + mode = 0 + else: + mode = 1 + elif b == 0x2d or digit(b): + if b == 0x2d: + i += 1 + ok = i < n and digit(d[i]) + if ok and d[i] == 0x30: + i += 1 + else: + while i < n and digit(d[i]): + i += 1 + if ok and i < n and d[i] == 0x2e: + i += 1 + ok = i < n and digit(d[i]) + while i < n and digit(d[i]): + i += 1 + if ok and i < n and d[i] in (0x65, 0x45): + i += 1 + if i < n and d[i] in (0x2b, 0x2d): + i += 1 + ok = i < n and digit(d[i]) + while i < n and digit(d[i]): + i += 1 + mode = 1 + 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) + mode = 1 + return False + + +if __name__ == '__main__': + import os, sys, time + suite = sys.argv[1] + files = sorted(os.listdir(suite)) + data = [open(os.path.join(suite, f), 'rb').read() for f in files] + for f, d in zip(files, data): + assert not f.startswith('y_') or valid(d), f + assert not f.startswith('n_') or not valid(d), f + big = b'[' + b','.join(b'{"key%d": [1.5e3, true, null, "text \\u00e9 %d"]}' % (k, k) for k in range(2000)) + b']' + runs = [] + for _ in range(7): + start = time.perf_counter() + valid(big) + runs.append((time.perf_counter() - start) * 1e9 / len(big)) + print('{"size":%d,"python_ns_per_byte":%.1f}' % (len(big), sorted(runs)[3])) diff --git a/research/experiments/044-specs/json_time.c b/research/experiments/044-specs/json_time.c new file mode 100644 index 0000000..b401ece --- /dev/null +++ b/research/experiments/044-specs/json_time.c @@ -0,0 +1,132 @@ +/* Research 140, design 027 stage 2a: aup's examples/json.aup (run by Pure) + * against the same validator in C, on the same document: both must say it is + * JSON, then each is timed (the median of seven runs of rounds checks). + * + * cc -O2 -std=c17 -I../../../core json_time.c ../../../build/libpure.a -lm -pthread -o json_time + * ./json_time json.pmod ROUNDS (PURE_JIT and PURE_BATCH choose Pure's path) + * + * The document is json_check.py'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> + +static int space(int b) { return b == 0x20 || b == 0x09 || b == 0x0a || b == 0x0d; } +static int digit(int b) { return b >= 0x30 && b <= 0x39; } +static int hex(int b) { return digit(b) || (b >= 0x61 && b <= 0x66) || (b >= 0x41 && b <= 0x46); } + +/* json.aup's design in C (its UTF-8 check left out: the document is ASCII). */ +static int valid(const unsigned char *d, size_t n, unsigned char *open) { + size_t i = 0, depth = 0; + int mode = 0, ok = 1; + while (ok) { + while (i < n && space(d[i])) ++i; + if (i == n) return mode == 1 && !depth; + int b = d[i]; + if (mode == 1) { + if (!depth) ok = 0; + else if (b == 0x2c) mode = open[depth - 1] == 0x5b ? 0 : 2; + else if (b == open[depth - 1] + 2) --depth; + else ok = 0; + ++i; + } else if ((mode == 3 && b == 0x7d) || (mode == 4 && b == 0x5d)) { + --depth, mode = 1, ++i; + } else if (mode == 2 || mode == 3) { + ok = b == 0x22, mode = 5; + } else if (b == 0x5b || b == 0x7b) { + open[depth++] = (unsigned char)b, mode = b == 0x5b ? 4 : 3, ++i; + } else if (b == 0x22 || mode == 5) { + ++i; + while (i < n && d[i] != 0x22 && ok) { + if (d[i] < 0x20) ok = 0; + else if (d[i] == 0x5c && i + 1 < n && d[i + 1] == 0x75) { + ok = i + 5 < n && hex(d[i + 2]) && hex(d[i + 3]) && hex(d[i + 4]) && hex(d[i + 5]); + i += 6; + } else if (d[i] == 0x5c) { + ok = i + 1 < n && strchr("\"\\/bfnrt", d[i + 1]) && d[i + 1]; + i += 2; + } else ++i; + } + ok = ok && i < n, ++i; + if (mode == 5) { + while (i < n && space(d[i])) ++i; + ok = ok && i < n && d[i] == 0x3a, ++i, mode = 0; + } else mode = 1; + } else if (b == 0x2d || digit(b)) { + if (b == 0x2d) ++i; + ok = i < n && digit(d[i]); + if (ok && d[i] == 0x30) ++i; + else while (i < n && digit(d[i])) ++i; + if (ok && i < n && d[i] == 0x2e) { + ++i, ok = i < n && digit(d[i]); + while (i < n && digit(d[i])) ++i; + } + if (ok && i < n && (d[i] == 0x65 || d[i] == 0x45)) { + ++i; + if (i < n && (d[i] == 0x2b || d[i] == 0x2d)) ++i; + ok = i < n && digit(d[i]); + while (i < n && digit(d[i])) ++i; + } + mode = 1; + } else { + const char *word = b == 0x74 ? "true" : b == 0x66 ? "false" : "null"; + size_t length = strlen(word); + ok = i + length <= n && !memcmp(d + i, word, length), i += length, mode = 1; + } + } + return 0; +} + +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 unsigned char doc[1 << 18], open[1 << 18]; + size_t n = 0; + doc[n++] = '['; + for (int k = 0; k < 2000; ++k) + n += (size_t)sprintf((char *)doc + n, "%s{\"key%d\": [1.5e3, true, null, \"text \\u00e9 %d\"]}", k ? "," : "", k, k); + doc[n++] = ']'; + 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){doc, n}) != PURE_OK) { fprintf(stderr, "event failed\n"); return 1; } + uint64_t work = app.work; + if (app.values[0].num != 1 || !valid(doc, n, open)) { fprintf(stderr, "not both valid\n"); return 1; } + double c_runs[7], pure_runs[7]; + volatile int sink = 0; + for (int r = 0; r < 7; ++r) { + double start = now_ns(); + for (int i = 0; i < rounds; ++i) sink += valid(doc, n, open); + 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){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; +} |
