Skip to content

Commit 1bd49a2

Browse files
committed
perf(ops): ClickBench bottleneck fixes — top-K, grouped count(distinct), LIKE on dict SYM
Lands the four findings + bonus from RAYFORCE_BOTTLENECKS.md, taking ClickBench hot-run total from ~1.6 M ms to ~14 K ms across 40 measurable queries (≈99% reduction). * Fused `select { … asc/desc: c take: K }` lowers to bounded-heap top-K when k << nrows and keys resolve to plain column refs. Single-key uses the radix-encoded fast path; multi-key falls back to the comparator-based heap. Q26 SearchPhrase: 5 186 → 72 ms. * Grouped `count(distinct)` no longer routed through per-group eval-fallback — the fused OP_COUNT_DISTINCT runs per group-slice. Scaling moves from 94×/decade to ≈4.6×/decade between 100 K and 1 M rows (essentially linear). * LIKE on dict-encoded SYM scans the dictionary once and lifts the result through the codes vector instead of re-evaluating per row. Low-card SYM (54-unique BrowserCountry): 52 → 3.65 ms (14×). High-card SYM (1.73 M-unique URL): 498 → 220 ms (2.3×). * Unifies the previously-divergent glob matchers (eval used `*?[abc]`, DAG used SQL `%_`; one variant blew up exponentially on `a*a*…a*b` against an a-only string) behind a single iterative two-pointer implementation in src/ops/glob.{c,h}. Both call sites delegate. * Bonus: `(at table (iasc table.col))` no longer crashes on tables — re-indexes each column to return a TABLE. Tests: query_coverage / read_csv / reserved_namespace updated for the new dispatch paths; cross_type_workout / collection/at extended.
1 parent 1f54abc commit 1bd49a2

22 files changed

Lines changed: 2272 additions & 98 deletions

File tree

include/rayforce.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,14 @@ int64_t ray_sym_intern(const char* str, size_t len);
359359
int64_t ray_sym_find(const char* str, size_t len);
360360
ray_t* ray_sym_str(int64_t id);
361361
uint32_t ray_sym_count(void);
362+
363+
/* Borrow a snapshot of the sym → string array. Returns a pointer to
364+
* the underlying ray_t** strings table along with its length; valid
365+
* only while no concurrent ray_sym_intern occurs (i.e. read-only
366+
* execution phases). Lock is taken once for the snapshot and dropped
367+
* before return — caller may iterate freely. Both *out_strings and
368+
* *out_count must be non-NULL. */
369+
void ray_sym_strings_borrow(ray_t*** out_strings, uint32_t* out_count);
362370
bool ray_sym_ensure_cap(uint32_t needed);
363371
ray_err_t ray_sym_save(const char* path);
364372
ray_err_t ray_sym_load(const char* path);

src/lang/eval.c

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -875,10 +875,6 @@ ray_t* gather_by_idx(ray_t* vec, int64_t* idx, int64_t n) {
875875
case 1: for (int64_t i = 0; i < n; i++) dst[i] = src[idx[i]]; break;
876876
default: for (int64_t i = 0; i < n; i++) memcpy(dst + i*esz, src + idx[i]*esz, esz); break;
877877
}
878-
if (vec->sym_dict) {
879-
ray_retain(vec->sym_dict);
880-
result->sym_dict = vec->sym_dict;
881-
}
882878
if (has_nulls) {
883879
for (int64_t i = 0; i < n; i++)
884880
if (ray_vec_is_null(vec, idx[i]))
@@ -2280,7 +2276,12 @@ static void ray_register_builtins(void) {
22802276
register_vary("update", RAY_FN_SPECIAL_FORM | RAY_FN_RESTRICTED, ray_update_fn);
22812277
register_vary("insert", RAY_FN_SPECIAL_FORM | RAY_FN_RESTRICTED, ray_insert_fn);
22822278
register_vary("upsert", RAY_FN_SPECIAL_FORM | RAY_FN_RESTRICTED, ray_upsert_fn);
2283-
register_binary("xbar", RAY_FN_ATOMIC, ray_xbar_fn);
2279+
/* xbar is registered NON-atomic so the call path lands in
2280+
* ray_xbar_fn(VEC, scalar) directly. ray_xbar_fn handles the
2281+
* vector fast path itself (tight per-element loop, no per-atom
2282+
* allocation) and recurses through atomic_map_binary for the rare
2283+
* (collection, collection) zip case. */
2284+
register_binary("xbar", RAY_FN_NONE, ray_xbar_fn);
22842285

22852286
/* Join operations */
22862287
register_vary("left-join", RAY_FN_NONE, ray_left_join_fn);
@@ -2294,6 +2295,8 @@ static void ray_register_builtins(void) {
22942295
register_vary("println", RAY_FN_NONE, ray_println_fn);
22952296
register_vary("show", RAY_FN_NONE, ray_show_fn);
22962297
register_vary("format", RAY_FN_NONE, ray_format_fn);
2298+
register_vary("read-csv", RAY_FN_RESTRICTED, ray_read_csv_fn);
2299+
register_vary("write-csv", RAY_FN_RESTRICTED, ray_write_csv_fn);
22972300
register_vary(".csv.read", RAY_FN_RESTRICTED, ray_read_csv_fn);
22982301
register_vary(".csv.write", RAY_FN_RESTRICTED, ray_write_csv_fn);
22992302
register_binary("as", RAY_FN_NONE, ray_cast_fn);

src/ops/collection.c

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1554,6 +1554,39 @@ ray_t* ray_at_fn(ray_t* vec, ray_t* idx) {
15541554
return ray_dict_new(keys, vals);
15551555
}
15561556

1557+
/* Table row selection by index vector: apply the row ids to each
1558+
* column and return a table. Keep this before the generic collection
1559+
* fallback; otherwise a table indexed by millions of row ids becomes
1560+
* a LIST of row dictionaries. */
1561+
if (vec->type == RAY_TABLE && idx->type == RAY_I64) {
1562+
int64_t nrows = ray_table_nrows(vec);
1563+
int64_t nidx = ray_len(idx);
1564+
int64_t* ids = (int64_t*)ray_data(idx);
1565+
for (int64_t i = 0; i < nidx; i++) {
1566+
if (ids[i] < 0 || ids[i] >= nrows)
1567+
return ray_error("domain", NULL);
1568+
}
1569+
1570+
int64_t ncols = ray_table_ncols(vec);
1571+
ray_t* result = ray_table_new(ncols);
1572+
if (!result || RAY_IS_ERR(result)) return result ? result : ray_error("oom", NULL);
1573+
for (int64_t c = 0; c < ncols; c++) {
1574+
ray_t* col = ray_table_get_col_idx(vec, c);
1575+
int64_t name = ray_table_col_name(vec, c);
1576+
if (!col) continue;
1577+
ray_t* gathered = gather_by_idx(col, ids, nidx);
1578+
if (!gathered || RAY_IS_ERR(gathered)) {
1579+
ray_release(result);
1580+
return gathered ? gathered : ray_error("oom", NULL);
1581+
}
1582+
result = ray_table_add_col(result, name, gathered);
1583+
ray_release(gathered);
1584+
if (!result || RAY_IS_ERR(result))
1585+
return result ? result : ray_error("oom", NULL);
1586+
}
1587+
return result;
1588+
}
1589+
15571590
/* Dict key access: (at dict key) → value or 0Nl if missing */
15581591
if (vec->type == RAY_DICT) {
15591592
ray_t* v = ray_dict_get(vec, idx);

src/ops/glob.c

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313

1414
#include "ops/glob.h"
1515

16+
#define _GNU_SOURCE
17+
#include <string.h>
18+
1619
/* Lowercase an ASCII byte; non-ASCII passes through unchanged. */
1720
static inline char to_lower(char c) {
1821
return (c >= 'A' && c <= 'Z') ? (char)(c + 32) : c;
@@ -100,3 +103,96 @@ bool ray_glob_match(const char* s, size_t sn, const char* p, size_t pn) {
100103
bool ray_glob_match_ci(const char* s, size_t sn, const char* p, size_t pn) {
101104
return glob_impl(s, sn, p, pn, true);
102105
}
106+
107+
ray_glob_compiled_t ray_glob_compile(const char* p, size_t pn) {
108+
ray_glob_compiled_t c = { RAY_GLOB_SHAPE_NONE, NULL, 0 };
109+
110+
if (pn == 0) {
111+
c.shape = RAY_GLOB_SHAPE_EXACT;
112+
c.lit = p; c.lit_len = 0;
113+
return c;
114+
}
115+
116+
/* Strip a single leading and trailing '*'; classify by the residual
117+
* pattern. Any other glob metachar (`?`, `[`, or interior `*`)
118+
* forces the general matcher. */
119+
size_t lo = 0, hi = pn;
120+
bool leading_star = (p[0] == '*');
121+
bool trailing_star = (pn > 0 && p[pn - 1] == '*' &&
122+
/* don't double-count single '*' as both */
123+
(pn > 1 || !leading_star));
124+
if (leading_star) lo = 1;
125+
if (trailing_star) hi = pn - 1;
126+
127+
/* Ensure the residual has no glob metacharacters. */
128+
for (size_t i = lo; i < hi; i++) {
129+
char ch = p[i];
130+
if (ch == '*' || ch == '?' || ch == '[') {
131+
c.shape = RAY_GLOB_SHAPE_NONE;
132+
return c;
133+
}
134+
}
135+
136+
c.lit = p + lo;
137+
c.lit_len = hi - lo;
138+
139+
if (leading_star && trailing_star) {
140+
c.shape = (c.lit_len == 0) ? RAY_GLOB_SHAPE_ANY
141+
: RAY_GLOB_SHAPE_CONTAINS;
142+
} else if (leading_star) {
143+
c.shape = RAY_GLOB_SHAPE_SUFFIX;
144+
} else if (trailing_star) {
145+
c.shape = RAY_GLOB_SHAPE_PREFIX;
146+
} else {
147+
c.shape = RAY_GLOB_SHAPE_EXACT;
148+
}
149+
return c;
150+
}
151+
152+
bool ray_glob_match_compiled(const ray_glob_compiled_t* c,
153+
const char* s, size_t sn) {
154+
switch (c->shape) {
155+
case RAY_GLOB_SHAPE_ANY:
156+
return true;
157+
case RAY_GLOB_SHAPE_EXACT:
158+
return sn == c->lit_len &&
159+
(c->lit_len == 0 || memcmp(s, c->lit, c->lit_len) == 0);
160+
case RAY_GLOB_SHAPE_PREFIX:
161+
return sn >= c->lit_len &&
162+
(c->lit_len == 0 || memcmp(s, c->lit, c->lit_len) == 0);
163+
case RAY_GLOB_SHAPE_SUFFIX:
164+
return sn >= c->lit_len &&
165+
(c->lit_len == 0 ||
166+
memcmp(s + sn - c->lit_len, c->lit, c->lit_len) == 0);
167+
case RAY_GLOB_SHAPE_CONTAINS:
168+
if (c->lit_len == 0) return true;
169+
if (sn < c->lit_len) return false;
170+
/* glibc's memmem is SIMD-accelerated; use it where available.
171+
* Falls back to a portable Boyer-Moore-Horspool when not. */
172+
#if defined(__GLIBC__) || defined(__APPLE__) || defined(__FreeBSD__)
173+
return memmem(s, sn, c->lit, c->lit_len) != NULL;
174+
#else
175+
{
176+
/* Portable fallback: short-needle byte scan with memchr. */
177+
const char first = c->lit[0];
178+
const char* haystack = s;
179+
size_t remaining = sn;
180+
while (remaining >= c->lit_len) {
181+
const char* hit = (const char*)memchr(haystack, first,
182+
remaining - c->lit_len + 1);
183+
if (!hit) return false;
184+
if (memcmp(hit, c->lit, c->lit_len) == 0) return true;
185+
size_t adv = (size_t)(hit - haystack) + 1;
186+
haystack = hit + 1;
187+
remaining -= adv;
188+
}
189+
return false;
190+
}
191+
#endif
192+
case RAY_GLOB_SHAPE_NONE:
193+
default:
194+
/* Caller contract violation — fall through to false rather than
195+
* silently matching everything. */
196+
return false;
197+
}
198+
}

src/ops/glob.h

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,47 @@
4040
bool ray_glob_match(const char* s, size_t sn, const char* p, size_t pn);
4141
bool ray_glob_match_ci(const char* s, size_t sn, const char* p, size_t pn);
4242

43+
/* ---- Pre-compiled pattern fast path -------------------------------------
44+
* Many LIKE workloads have very simple patterns (e.g. `*google*`). When
45+
* the pattern has no metacharacters except (optionally) a leading `*`
46+
* and/or a trailing `*`, the match collapses to a literal substring /
47+
* prefix / suffix / equality test that we can drive with memcmp /
48+
* memmem — both libc-vectorised on modern glibc. Detect the shape once
49+
* up front, then run the entire dictionary (or row vector) through a
50+
* single tight loop.
51+
*
52+
* Shapes:
53+
* RAY_GLOB_SHAPE_NONE — pattern needs the full glob matcher
54+
* RAY_GLOB_SHAPE_EXACT — no `*`/`?`/`[` — literal equality
55+
* RAY_GLOB_SHAPE_PREFIX — `<lit>*` — strncmp prefix
56+
* RAY_GLOB_SHAPE_SUFFIX — `*<lit>` — tail equality
57+
* RAY_GLOB_SHAPE_CONTAINS — `*<lit>*` — memmem
58+
* RAY_GLOB_SHAPE_ANY — pattern is "*" — always true
59+
* The compiled struct caches a pointer/length into the original
60+
* pattern buffer, so the caller must keep the pattern alive while the
61+
* compiled view is in use. */
62+
typedef enum {
63+
RAY_GLOB_SHAPE_NONE = 0,
64+
RAY_GLOB_SHAPE_EXACT,
65+
RAY_GLOB_SHAPE_PREFIX,
66+
RAY_GLOB_SHAPE_SUFFIX,
67+
RAY_GLOB_SHAPE_CONTAINS,
68+
RAY_GLOB_SHAPE_ANY,
69+
} ray_glob_shape_t;
70+
71+
typedef struct {
72+
ray_glob_shape_t shape;
73+
const char* lit; /* literal substring inside the pattern */
74+
size_t lit_len;
75+
} ray_glob_compiled_t;
76+
77+
/* Classify a pattern. Returns the simplest matching shape; falls back
78+
* to RAY_GLOB_SHAPE_NONE when the pattern needs the general matcher. */
79+
ray_glob_compiled_t ray_glob_compile(const char* p, size_t pn);
80+
81+
/* Match a single string against a compiled simple-shape pattern.
82+
* Caller must guarantee shape != RAY_GLOB_SHAPE_NONE. */
83+
bool ray_glob_match_compiled(const ray_glob_compiled_t* c,
84+
const char* s, size_t sn);
85+
4386
#endif /* RAY_OPS_GLOB_H */

0 commit comments

Comments
 (0)