Speed up JSON.parse and JSON.stringify scanning (#1721)

This commit is contained in:
Chris Tate
2026-10-21 17:34:58 -05:00
committed by GitHub
+139 -57
View File
@@ -311,9 +311,10 @@ void scr_jb_put_f64(ScrJsonBuf *b, double v) {
/* Find the next quote, backslash or control byte. memcpy keeps the word
* loads unaligned-safe, and the length guard never reads past the input.
* The masks only decide whether to inspect a word byte by byte: borrow
* propagation may mark extra lanes, so it must not determine a byte index.
* High UTF-8 bytes are ordinary data, independent of host byte order. */
* Borrow propagation can mark extra lanes, but only above a lane that
* holds a special byte, so on little-endian hosts the lowest marked lane
* is the answer; other hosts inspect the marked word byte by byte. High
* UTF-8 bytes are ordinary data. */
static size_t scr_json_plain_bytes(const char *data, size_t len) {
const uint64_t ones = UINT64_C(0x0101010101010101);
const uint64_t high = UINT64_C(0x8080808080808080);
@@ -326,9 +327,41 @@ static size_t scr_json_plain_bytes(const char *data, size_t len) {
uint64_t special = ((quotes - ones) & ~quotes) |
((slashes - ones) & ~slashes) |
((word - ones * 0x20) & ~word);
if (special & high) break;
special &= high;
if (special) {
#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
/* A borrow only marks lanes above a lane that really matched, so
* the lowest marked lane is the first special byte. */
return i + (size_t)__builtin_ctzll(special) / 8;
#else
break;
#endif
}
i += sizeof word;
}
/* The last 1..7 bytes as one word: two overlapping 4-byte loads, or the
* first, middle and last byte of a shorter rest with the remaining lanes
* filled with a plain byte. A plain rest (the common case: most strings
* are short) ends the scan; otherwise the bytes are inspected one by one. */
size_t rest = len - i;
if (rest) {
uint64_t word;
if (rest >= 4) {
uint32_t lo, hi;
memcpy(&lo, data + i, sizeof lo);
memcpy(&hi, data + len - 4, sizeof hi);
word = (uint64_t)lo | (uint64_t)hi << 32;
} else {
word = (uint64_t)(unsigned char)data[i] | (uint64_t)(unsigned char)data[i + rest / 2] << 8 |
(uint64_t)(unsigned char)data[len - 1] << 16 | UINT64_C(0x4141414141000000);
}
uint64_t quotes = word ^ (ones * '"');
uint64_t slashes = word ^ (ones * '\\');
uint64_t special = ((quotes - ones) & ~quotes) |
((slashes - ones) & ~slashes) |
((word - ones * 0x20) & ~word);
if (!(special & high)) return len;
}
while (i < len) {
unsigned char c = (unsigned char)data[i];
if (c < 0x20 || c == '"' || c == '\\') break;
@@ -9008,6 +9041,11 @@ typedef struct {
ScrDynRegion *region; /* tags completed containers (ScrDynRegion); may be NULL */
struct ScrJsonShape *shapes; /* key-sequence transitions (scr_json_object) */
uint32_t nshapes, shape_cap;
/* Schema records whose plain field names are known (scr_jschema_plain):
* bit i of the mask is set when field i's name holds no byte JSON text
* would escape. */
const struct ScrJsonSchema *plain_schema[4];
uint64_t plain_mask[4];
} ScrJsonP;
static void scr_json_throw(const ScrJsonP *p, const char *msg) {
@@ -9015,7 +9053,10 @@ static void scr_json_throw(const ScrJsonP *p, const char *msg) {
scr_throw_error_msg(SCR_ERR_SYNTAX, msg, strlen(msg));
}
static void scr_json_throw_pos(const ScrJsonP *p, const char *what, size_t pos) {
/* Errors stay out of line so the scanners that report them keep their
* registers for the common path. */
static __attribute__((noinline, cold)) void scr_json_throw_pos(const ScrJsonP *p, const char *what,
size_t pos) {
if (p->quiet) return;
char buf[128];
int n = snprintf(buf, sizeof buf, "%s in JSON at position %zu", what, pos);
@@ -9036,11 +9077,15 @@ static void scr_json_throw_token(const ScrJsonP *p) {
}
static void scr_json_ws(ScrJsonP *p) {
while (p->pos < p->len) {
char c = p->s[p->pos];
if (c == ' ' || c == '\t' || c == '\n' || c == '\r') p->pos++;
/* A local cursor, as in scr_json_number_value. */
const char *s = p->s;
size_t pos = p->pos;
while (pos < p->len) {
char c = s[pos];
if (c == ' ' || c == '\t' || c == '\n' || c == '\r') pos++;
else break;
}
p->pos = pos;
}
/* Append the WTF-8 encoding of a code point or lone surrogate unit; a low
@@ -9191,7 +9236,14 @@ static const double scr_json_pow10[23] = {
* (thrown unless quiet). Shared by the dyn tree and the schema parser so
* both produce bit-identical values. */
static bool scr_json_number_value(ScrJsonP *p, double *out) {
size_t start = p->pos;
/* The cursor and text stay in locals: the runtime builds without strict
* aliasing, so a byte read through p->s could alias p->pos, and a loop
* on p->pos would store it on every digit. p->pos is written once, when
* the token ends. */
const char *s = p->s;
const size_t len = p->len;
const size_t start = p->pos;
size_t pos = start;
/* Grammar validation and value accumulation in one pass. Clinger's fast
* path: with at most 15 significant digits the mantissa is exact in a
* double, and scaling by an exact power of ten (|exp| <= 22) rounds
@@ -9200,35 +9252,37 @@ static bool scr_json_number_value(ScrJsonP *p, double *out) {
int ndig = 0; /* significant digits folded into mant */
int exp10 = 0; /* decimal exponent (fraction shift + explicit exponent) */
bool neg = false, precise = true;
if (p->s[p->pos] == '-') {
if (s[pos] == '-') {
neg = true;
p->pos++;
if (p->pos >= p->len || p->s[p->pos] < '0' || p->s[p->pos] > '9') {
scr_json_throw_pos(p, "No number after minus sign", p->pos);
pos++;
if (pos >= len || s[pos] < '0' || s[pos] > '9') {
p->pos = pos;
scr_json_throw_pos(p, "No number after minus sign", pos);
return false;
}
}
if (p->s[p->pos] == '0') {
p->pos++;
if (s[pos] == '0') {
pos++;
} else {
while (p->pos < p->len && p->s[p->pos] >= '0' && p->s[p->pos] <= '9') {
while (pos < len && s[pos] >= '0' && s[pos] <= '9') {
if (ndig < 15) {
mant = mant * 10 + (uint64_t)(p->s[p->pos] - '0');
mant = mant * 10 + (uint64_t)(s[pos] - '0');
ndig++;
} else {
precise = false;
}
p->pos++;
pos++;
}
}
if (p->pos < p->len && p->s[p->pos] == '.') {
p->pos++;
if (p->pos >= p->len || p->s[p->pos] < '0' || p->s[p->pos] > '9') {
scr_json_throw_pos(p, "Unterminated fractional number", p->pos);
if (pos < len && s[pos] == '.') {
pos++;
if (pos >= len || s[pos] < '0' || s[pos] > '9') {
p->pos = pos;
scr_json_throw_pos(p, "Unterminated fractional number", pos);
return false;
}
while (p->pos < p->len && p->s[p->pos] >= '0' && p->s[p->pos] <= '9') {
unsigned digit = (unsigned)(p->s[p->pos] - '0');
while (pos < len && s[pos] >= '0' && s[pos] <= '9') {
unsigned digit = (unsigned)(s[pos] - '0');
if (mant == 0 && digit == 0) {
exp10--; /* leading fractional zeros scale without a digit */
} else if (ndig < 15) {
@@ -9238,29 +9292,33 @@ static bool scr_json_number_value(ScrJsonP *p, double *out) {
} else {
precise = false;
}
p->pos++;
pos++;
}
}
if (p->pos < p->len && (p->s[p->pos] == 'e' || p->s[p->pos] == 'E')) {
p->pos++;
if (pos < len && (s[pos] == 'e' || s[pos] == 'E')) {
pos++;
bool eneg = false;
if (p->pos < p->len && (p->s[p->pos] == '+' || p->s[p->pos] == '-')) {
eneg = p->s[p->pos] == '-';
p->pos++;
if (pos < len && (s[pos] == '+' || s[pos] == '-')) {
eneg = s[pos] == '-';
pos++;
}
if (p->pos >= p->len || p->s[p->pos] < '0' || p->s[p->pos] > '9') {
scr_json_throw_pos(p, "Exponent part is missing a number", p->pos);
if (pos >= len || s[pos] < '0' || s[pos] > '9') {
p->pos = pos;
scr_json_throw_pos(p, "Exponent part is missing a number", pos);
return false;
}
int ev = 0;
while (p->pos < p->len && p->s[p->pos] >= '0' && p->s[p->pos] <= '9') {
if (ev < 100000) ev = ev * 10 + (p->s[p->pos] - '0');
p->pos++;
while (pos < len && s[pos] >= '0' && s[pos] <= '9') {
if (ev < 100000) ev = ev * 10 + (s[pos] - '0');
pos++;
}
exp10 += eneg ? -ev : ev;
}
p->pos = pos;
if (precise && exp10 >= -22 && exp10 <= 22) {
double v = (double)mant; /* exact: mant < 10^15 < 2^53 */
/* exact: mant < 10^15 < 2^53; the signed conversion is one
* instruction on x86-64, the unsigned one a sequence */
double v = (double)(int64_t)mant;
if (exp10 > 0) v *= scr_json_pow10[exp10];
else if (exp10 < 0) v /= scr_json_pow10[-exp10];
*out = neg ? -v : v;
@@ -9270,7 +9328,7 @@ static bool scr_json_number_value(ScrJsonP *p, double *out) {
* grammar above is a strict subset of what strtod accepts). The ScrStr
* data is NUL-terminated, and strtod stops at the first non-number char,
* so parsing from `start` reads exactly the validated token. */
*out = strtod(p->s + start, NULL);
*out = strtod(s + start, NULL);
return true;
}
@@ -9867,26 +9925,38 @@ static __attribute__((noinline)) bool scr_jschema_absent(const ScrJsonSchema *s,
return true;
}
/* The quoted key at p->pos when it is exactly `f`'s name, written without
* escapes: the bytes consumed (name length + 2), else 0. Names of more
* than eight bytes, and names holding a byte JSON text would escape,
* take the general key scan. */
/* Bit i set when field i's name (at most eight bytes) holds no quote,
* backslash or control byte, so text that spells it unescaped is exactly
* that key. Names are schema constants: each record schema computes its
* mask once per parse, in a four-entry table. */
static uint64_t scr_jschema_plain(ScrJsonP *p, const ScrJsonSchema *s) {
size_t slot = ((uintptr_t)s >> 4) & 3;
if (p->plain_schema[slot] == s) return p->plain_mask[slot];
uint64_t mask = 0;
for (size_t i = 0; i < s->nfields && i < 64; i++) {
const ScrJsonSchemaField *f = &s->fields[i];
bool plain = f->name_len > 0 && f->name_len <= 8;
for (size_t k = 0; plain && k < f->name_len; k++) {
unsigned char c = (unsigned char)f->name[k];
plain = c >= 0x20 && c != '"' && c != '\\';
}
if (plain) mask |= (uint64_t)1 << i;
}
p->plain_schema[slot] = s;
p->plain_mask[slot] = mask;
return mask;
}
/* The quoted key at p->pos when it is exactly `f`'s name, a plain name of
* at most eight bytes (scr_jschema_plain): the bytes consumed (name
* length + 2), else 0. Other names take the general key scan. */
static inline size_t scr_jschema_key_direct(const ScrJsonP *p, const ScrJsonSchemaField *f) {
size_t len = f->name_len;
size_t at = p->pos + 1;
if (len == 0 || len > 8 || len >= p->len - at) return 0;
if (len >= p->len - at) return 0;
const char *text = p->s + at;
uint64_t word = scr_key_short_word(f->name, len);
if (text[len] != '"' || scr_key_short_word(text, len) != word) return 0;
/* The packed word holds every name byte (lengths one to three use its
* low three bytes). A quote, backslash or control byte sends the key to
* the general scan, as in scr_json_plain_bytes. */
const uint64_t ones = UINT64_C(0x0101010101010101), high = UINT64_C(0x8080808080808080);
uint64_t quotes = word ^ (ones * '"'), slashes = word ^ (ones * '\\');
uint64_t special = ((quotes - ones) & ~quotes) | ((slashes - ones) & ~slashes) |
((word - ones * 0x20) & ~word);
uint64_t bytes = len >= 4 ? ~UINT64_C(0) : UINT64_C(0xffffff);
return (special & high & bytes) ? 0 : len + 2;
if (text[len] != '"' || scr_key_short_word(text, len) != scr_key_short_word(f->name, len)) return 0;
return len + 2;
}
/* An undeclared member of a record with an index signature, after its
@@ -10014,6 +10084,7 @@ static bool scr_jschema_record(ScrJsonP *p, const ScrJsonSchema *s, ScrJsonSlot
const uint64_t all = n == 64 ? ~(uint64_t)0 : (((uint64_t)1 << n) - 1);
uint64_t seen = 0;
size_t guess = 0;
const uint64_t plain = scr_jschema_plain(p, s);
scr_json_ws(p);
if (p->pos < p->len && p->s[p->pos] == '}') {
p->pos++;
@@ -10025,7 +10096,8 @@ static bool scr_jschema_record(ScrJsonP *p, const ScrJsonSchema *s, ScrJsonSlot
/* Members usually arrive in declaration order: try the successor of
* the previous match, directly against the text, before scanning. */
size_t idx = n;
size_t direct = guess < n ? scr_jschema_key_direct(p, &s->fields[guess]) : 0;
size_t direct =
guess < n && (plain >> guess & 1) ? scr_jschema_key_direct(p, &s->fields[guess]) : 0;
if (direct) {
idx = guess;
p->pos += direct;
@@ -10104,13 +10176,21 @@ static bool scr_jschema_array(ScrJsonP *p, const ScrJsonSchema *s, ScrJsonSlot *
out->ptr = arr;
return true;
}
/* Elements gather in a local run and append in bulk (scr_arr_push_many,
* which moves references in as scr_arr_push_ref does). */
uint64_t run[32];
size_t n = 0;
for (;;) {
ScrJsonSlot v;
if (!scr_jschema_member(p, elem, &v)) goto fail;
switch (elem->kind) {
case SCR_JSCHEMA_F64: scr_arr_push_f64(arr, v.f); break;
case SCR_JSCHEMA_BOOL: scr_arr_push_bool(arr, v.b); break;
default: scr_arr_push_ref(arr, v.ptr); break; /* moves in */
case SCR_JSCHEMA_F64: memcpy(&run[n++], &v.f, sizeof v.f); break;
case SCR_JSCHEMA_BOOL: run[n++] = v.b ? 1 : 0; break;
default: run[n++] = (uint64_t)(uintptr_t)v.ptr; break;
}
if (n == sizeof run / sizeof run[0]) {
scr_arr_push_many(arr, n, run);
n = 0;
}
scr_json_ws(p);
if (p->pos >= p->len) goto fail;
@@ -10120,10 +10200,12 @@ static bool scr_jschema_array(ScrJsonP *p, const ScrJsonSchema *s, ScrJsonSlot *
}
if (p->s[p->pos] != ']') goto fail;
p->pos++;
scr_arr_push_many(arr, n, run);
out->ptr = arr;
return true;
}
fail:
scr_arr_push_many(arr, n, run); /* the array releases what it holds */
s->release(arr);
return false;
}