mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-06 08:23:19 +08:00
Speed up JSON.parse and JSON.stringify scanning (#1721)
This commit is contained in:
+139
-57
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user