Speed up split, indexOf, slice and Number() on ASCII fields (#1710)

This commit is contained in:
Chris Tate
2026-10-21 16:16:14 -05:00
committed by GitHub
3 changed files with 138 additions and 18 deletions
+89 -18
View File
@@ -1769,14 +1769,9 @@ static inline double scr_to_integer_or_infinity(double x) {
return isnan(x) ? 0.0 : x;
}
/* Byte substring search (needle and hay are both canonical WTF-8,
* so any byte-level match starts on a char boundary — UTF-8 is
* self-synchronizing). Empty needle matches at hay. */
static const char *scr_byte_find(const char *hay, size_t hay_len,
const char *nee, size_t nee_len) {
if (nee_len == 0) return hay;
if (nee_len > hay_len) return NULL;
if (nee_len == 1) return memchr(hay, (unsigned char)nee[0], hay_len);
/* scr_byte_find for a needle of at least two bytes, no longer than hay. */
static __attribute__((noinline)) const char *scr_byte_find_long(const char *hay, size_t hay_len,
const char *nee, size_t nee_len) {
const unsigned char first = (unsigned char)nee[0];
const unsigned char second = (unsigned char)nee[1];
/* The last possible match start. */
@@ -1826,6 +1821,18 @@ static const char *scr_byte_find(const char *hay, size_t hay_len,
return NULL;
}
/* Byte substring search (needle and hay are both canonical WTF-8,
* so any byte-level match starts on a char boundary — UTF-8 is
* self-synchronizing). Empty needle matches at hay. The one-byte case,
* every split on a separator character, goes to memchr without a frame. */
static inline const char *scr_byte_find(const char *hay, size_t hay_len,
const char *nee, size_t nee_len) {
if (nee_len == 0) return hay;
if (nee_len > hay_len) return NULL;
if (nee_len == 1) return memchr(hay, (unsigned char)nee[0], hay_len);
return scr_byte_find_long(hay, hay_len, nee, nee_len);
}
/* ── searches for a needle that can match half of a surrogate pair ─────
* Canonical storage makes byte matches exact for every needle except one
* that starts with a lone low surrogate or ends with a lone high one: JS
@@ -2020,6 +2027,13 @@ double scr_str_char_code_at(ScrStr *s, double i) {
}
double scr_str_index_of(ScrStr *s, ScrStr *needle, double fromIndex) {
/* A search from the start of a proven-ASCII receiver: byte offsets are
* UTF-16 indexes, and a needle with other bytes is never found. A start
* index that is not positive (NaN, -0 or negative) clamps to 0. */
if (s->ascii && !(fromIndex > 0)) {
const char *found = scr_byte_find(s->data, s->len, needle->data, needle->len);
return found ? (double)(found - s->data) : -1.0;
}
double pos = scr_to_integer_or_infinity(fromIndex);
if (needle->len > s->len) return -1.0;
ScrSidx *e = scr_sidx_for(s);
@@ -2142,6 +2156,12 @@ static size_t scr_slice_boundary(double v, size_t len16) {
}
ScrStr *scr_str_slice(ScrStr *s, double start, double end) {
/* A proven-ASCII receiver needs no index map (scr_str_slice_units). */
if (s->ascii) {
size_t from = scr_slice_boundary(scr_to_integer_or_infinity(start), s->len);
size_t to = scr_slice_boundary(scr_to_integer_or_infinity(end), s->len);
return scr_str_slice_units(s, NULL, from, to);
}
ScrSidx *e = scr_sidx_for(s);
size_t len16 = scr_sidx_len(s, e);
size_t from = scr_slice_boundary(scr_to_integer_or_infinity(start), len16);
@@ -2407,8 +2427,10 @@ void scr_str_split_cursor_init(ScrSplitCursor *cursor, uint32_t limit) {
cursor->pending_unit = 0;
}
static ScrStr *scr_str_split_piece(const char *bytes, size_t len, ScrStr **scratch) {
if (!scratch || len <= 4) return scr_str_from_span(bytes, len);
/* `ascii`: the bytes are a span of a proven-ASCII string. */
static ScrStr *scr_str_split_piece(const char *bytes, size_t len, ScrStr **scratch, bool ascii) {
if (!scratch || len <= 4) return ascii ? scr_str_from_span_ascii(bytes, len)
: scr_str_from_span(bytes, len);
ScrStr *piece = *scratch;
if (piece && piece->rc == 1 && piece->cap >= len) {
/* The scratch owner is the only remaining reference. Invalidate all
@@ -2419,10 +2441,12 @@ static ScrStr *scr_str_split_piece(const char *bytes, size_t len, ScrStr **scrat
memcpy(piece->data, bytes, len);
piece->len = len;
piece->data[len] = '\0';
piece->ascii = ascii;
} else {
ScrStr *fresh = scr_str_alloc_raw(len, len < 64 ? 64 : len);
memcpy(fresh->data, bytes, len);
fresh->data[len] = '\0';
fresh->ascii = ascii;
*scratch = fresh;
scr_str_release(piece);
piece = fresh;
@@ -2480,7 +2504,7 @@ const char *scr_str_split_cursor_span(ScrStr *s, ScrStr *sep, ScrSplitCursor *cu
}
ScrStr *scr_str_split_materialize(const char *bytes, size_t length, ScrStr **scratch) {
return scr_str_split_piece(bytes, length, scratch);
return scr_str_split_piece(bytes, length, scratch, false);
}
ScrStr *scr_str_split_cursor_next(ScrStr *s, ScrStr *sep, ScrSplitCursor *cursor,
@@ -2488,7 +2512,7 @@ ScrStr *scr_str_split_cursor_next(ScrStr *s, ScrStr *sep, ScrSplitCursor *cursor
size_t len;
const char *bytes = scr_str_split_span(s, sep, cursor, sep->len == 0, &len);
if (!bytes) return NULL;
return scr_str_split_piece(bytes, len, scratch);
return scr_str_split_piece(bytes, len, scratch, s->ascii);
}
/* Move a bounded batch of pieces into array storage in one operation.
@@ -2513,7 +2537,8 @@ static void scr_str_split_fill(ScrArr *out, ScrStr *s, ScrStr *sep,
size_t count = 0, len;
const char *bytes;
while (count < 64 && (bytes = scr_str_split_span(s, sep, cursor, units, &len))) {
ScrStr *piece = scr_str_from_span(bytes, len);
ScrStr *piece =
s->ascii ? scr_str_from_span_ascii(bytes, len) : scr_str_from_span(bytes, len);
slots[count++] = (uint64_t)(uintptr_t)piece;
}
if (count) scr_arr_push_many(out, count, slots);
@@ -2522,18 +2547,43 @@ static void scr_str_split_fill(ScrArr *out, ScrStr *s, ScrStr *sep,
}
ScrArr *scr_str_split_limit(ScrStr *s, ScrStr *sep, double limit_num) {
uint32_t limit = scr_to_uint32(limit_num);
/* split(sep) passes the default limit, 2^32 - 1. */
uint32_t limit = limit_num == 4294967295.0 ? UINT32_MAX : scr_to_uint32(limit_num);
/* An empty separator yields one piece per UTF-16 unit: size it once. */
size_t pieces = 0;
if (sep->len == 0) {
pieces = s->ascii ? s->len : scr_sidx_len(s, scr_sidx(s));
if (pieces > limit) pieces = limit;
}
ScrArr *out = scr_arr_new(SCR_ELEM_STR, pieces);
ScrSplitCursor cursor;
scr_str_split_cursor_init(&cursor, limit);
if (sep->len == 0) scr_str_split_fill(out, s, sep, &cursor, true);
else scr_str_split_fill(out, s, sep, &cursor, false);
if (sep->len == 0) {
ScrArr *out = scr_arr_new(SCR_ELEM_STR, pieces);
scr_str_split_fill(out, s, sep, &cursor, true);
return out;
}
/* Most splits fit one batch: its count sizes the result, whose slots then
* share the header's block (scr_arr_new_inline). A longer split appends
* the rest batch by batch. */
uint64_t slots[64];
size_t count = 0, len;
const char *bytes;
while (count < 64 && (bytes = scr_str_split_span(s, sep, &cursor, false, &len)))
slots[count++] = (uint64_t)(uintptr_t)(s->ascii ? scr_str_from_span_ascii(bytes, len)
: scr_str_from_span(bytes, len));
ScrArr *out = scr_arr_new_inline(SCR_ELEM_STR, count);
if (count) {
/* A fresh inline array of exactly `count` slots: store the pieces and
* their states directly. */
uint8_t *states = scr_arr_states_end(out);
for (size_t k = 0; k < count; k++) {
out->data[k] = slots[k];
*scr_arr_state_in(states, k) = SCR_ARR_VALUE;
}
out->len = (uint32_t)count;
out->filled = (uint32_t)count;
}
if (count == 64) scr_str_split_fill(out, s, sep, &cursor, false);
return out;
}
@@ -3104,7 +3154,7 @@ double scr_string_to_number(ScrStr *s) {
return scr_bytes_to_number(s->data, s->len);
}
double scr_bytes_to_number(const char *p, size_t len) {
static __attribute__((noinline)) double scr_bytes_to_number_any(const char *p, size_t len) {
size_t b = 0, e = len;
while (b < e) {
unsigned char c = (unsigned char)p[b];
@@ -3164,6 +3214,27 @@ double scr_bytes_to_number(const char *p, size_t len) {
return r;
}
/* An optional sign and 1 to 15 decimal digits, the common shape of a
* parsed field, is an exact integer below 2^53 with no whitespace to trim
* and no other grammar to apply ("-0" is -0, leading zeros are decimal).
* Everything else takes the full grammar. */
double scr_bytes_to_number(const char *p, size_t len) {
if (len - 1 < 16) {
size_t i = p[0] == '-' || p[0] == '+';
if (i < len && len - i <= 15) {
int64_t m = 0;
size_t k = i;
for (; k < len; k++) {
unsigned d = (unsigned)(unsigned char)p[k] - '0';
if (d > 9) break;
m = m * 10 + d;
}
if (k == len) return p[0] == '-' ? -(double)m : (double)m;
}
}
return scr_bytes_to_number_any(p, len);
}
ScrStr *scr_f64_to_scrstr(double x) {
if (x >= 0 && x < 100) { /* -0 prints "0" as well */
unsigned v = (unsigned)x;
@@ -0,0 +1,48 @@
// Field parsing on ASCII text: Number() on digit strings (signs, zeros,
// 15 and 16 digits, whitespace and other grammar), indexOf from the start
// and from other positions, slice bounds, and split results of every
// size, including more pieces than one batch and results that grow.
const numbers = [
"0", "-0", "+0", "7", "-7", "+42", "007", "-007", "123456789012345", "-123456789012345",
"1234567890123456", "9007199254740993", "-", "+", "", " 12", "12 ", "\t3\n", "1e3",
"1.5", "0x1f", "-0x1f", "12px", "1_000", "Infinity", "-Infinity", "+-1", "--1",
];
for (const text of numbers) {
const value = Number(text);
console.log(JSON.stringify(text), value, Object.is(value, -0));
}
const line = "/route/17?request=123 200 45 3200";
console.log(line.indexOf("?"), line.indexOf(" "), line.indexOf("x"), line.indexOf(""));
console.log(line.indexOf("?", -5), line.indexOf("?", NaN), line.indexOf("?", 10), line.indexOf(" ", 30));
console.log(line.indexOf("", 100), line.indexOf("", -1), "".indexOf(""), "".indexOf("a"));
console.log(line.indexOf("é"), "aé?".indexOf("?"), "aé?".indexOf("é"));
console.log(line.slice(0, line.indexOf("?")), line.slice(-4), line.slice(5, 2), line.slice(-100, 3));
console.log(line.slice(NaN, Infinity) === line, line.slice(2, -2), "héllo".slice(1, 3));
const fields = line.split(" ");
console.log(fields.length, fields.join("|"));
fields.push("extra");
fields[0] = fields[0]!.slice(0, 6);
console.log(fields.length, fields.join("|"));
const many = Array.from({ length: 150 }, (_, i) => String(i * 7)).join(",");
const pieces = many.split(",");
console.log(pieces.length, pieces[0], pieces[63], pieces[64], pieces[149]);
let sum = 0;
for (const piece of pieces) sum += Number(piece);
console.log(sum);
console.log("a,b,,c,".split(","), ",".split(","), "".split(","), "abc".split(",", 0));
console.log("a b c d".split(" ", 2), "x--y--z".split("--"), "héllo wörld".split(" "));
const rows = "/a?x=1 200 5 10\n/b 404 7 20\n/a?y=2 500 9 30\n";
const totals = new Map<string, number>();
for (const row of rows.split("\n")) {
const parts = row.split(" ");
if (parts.length !== 4) continue;
const raw = parts[0]!;
const query = raw.indexOf("?");
const route = query < 0 ? raw : raw.slice(0, query);
totals.set(route, (totals.get(route) ?? 0) + Number(parts[1]) + Number(parts[3]));
}
console.log([...totals.entries()].map(([route, total]) => route + "=" + total).join(";"));
@@ -0,0 +1 @@
{ "plain": 3899, "san": 5479 }