1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
/*
* Copyright (c) 2025-2026 Anton Kundenko <singaraiona@gmail.com>
* All rights reserved.
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/**
* Probabilistic cardinality sketch (HyperLogLog).
*
* Each sketch holds 2^P registers; each register stores the maximum
* leading-zero count (rho) seen for any hash whose top P bits index
* that register. Cardinality is then read off the harmonic mean of
* 2^reg over all registers, with bias correction for both ends of
* the range. Standard error ≈ 1.04 / sqrt(2^P). P=14 → ≈ 0.8 %.
*
* Memory: 1 byte per register (8-bit reg holds rho up to 64+P, way
* over the 6 bits a packed implementation would need; the extra few
* KB buys a tighter hot loop). At P=14 a sketch is 16 KB and lives
* in L2 for the duration of one query.
*
* Sparse representation:
* Per-group HLL at high group counts wants to amortise the 16 KB
* sketch across groups that may only see a handful of hashes each
* (q13 SearchPhrase × UserID: many groups with < 50 uniques). In
* sparse mode the sketch stores only the registers that have been
* written, as 32-bit `(reg_idx << 8) | rho` entries in a small
* caller-provided buffer. The estimate / merge paths transparently
* support both modes; sparse converts to dense when the entry count
* exceeds the cap (caller-supplied; the per-group kernel uses 256).
*
* The sketch is mergeable element-wise (max), which is the property
* the per-group / per-worker aggregation paths rely on: each worker
* builds a local sketch and the planner merges them at finalisation.
*/
/* Default precision: 14 (16384 registers, ~0.81 % std error, 16 KB). */
/* Sparse cap for per-group sketches. Each entry is 4 bytes, so the
* sparse buffer is 1 KB at this cap — well inside L1 and 16× smaller
* than the dense register array. Above the cap, sparse is converted
* to dense in place (caller supplies both buffers on the stack). */
typedef struct ray_hll_t;
/* Initialise an empty *dense* sketch with `p` precision bits. Allocates
* regs via scratch_alloc; the caller frees with ray_hll_free. Returns
* 0 on success, -1 on OOM. */
int ;
/* Initialise an empty *sparse* sketch with caller-provided buffers.
* sparse_buf — buffer of size sparse_cap entries, used as the sparse
* set until conversion to dense.
* dense_buf — buffer of size 1<<p bytes, populated on conversion.
* Both buffers are typically stack-allocated by the worker task. The
* sketch starts sparse (regs == NULL). No allocation occurs; this
* never fails. Caller does not need to call ray_hll_free. */
void ;
/* Free the regs allocation. Safe on a zeroed (uninitialised) sketch.
* Sparse sketches with caller-provided buffers have _hdr == NULL and
* are a no-op here — they're freed implicitly when the stack frame
* unwinds. */
void ;
/* Zero all registers (clears the sketch — same effect as init with the
* same p, but in-place; useful when reusing a sketch across calls).
* Resets to sparse mode if a sparse buffer is attached. */
void ;
/* Sparse → dense conversion. Replays sparse_keys into the (already-
* attached) dense buffer, zeros remaining registers, clears sparse_count.
* Out-of-line: only called when the sparse cap is hit. */
void ;
/* Add a 64-bit hash to the sketch. Caller is responsible for hashing
* its value type before invoking — see ray_hash_i64 / ray_hash_bytes
* in ops/hash.h. Hot path; kept fully inline. Dense fast path is
* marked likely; the sparse arm is the fallback for per-group sketches
* that haven't yet exceeded RAY_HLL_SPARSE_CAP. */
static inline void
/* Merge src into dst (element-wise max). src and dst must share the
* same precision p. Handles all four (dense/sparse)×(dense/sparse)
* combinations; sparse+sparse promotes dst to dense first so the
* merged sketch remains a valid dense register array. */
void ;
/* Estimate the unique-value count of all hashes added so far. Uses
* the standard HyperLogLog estimator with bias-corrected raw-mean for
* the mid-range and linear counting (m * ln(m/V)) when many registers
* are still zero (V = unused register count). Branches on mode:
* dense scans the register array; sparse iterates the entry set and
* accounts for (m - sparse_count) unset registers analytically. */
int64_t ;
/* Scalar approximate `count(distinct …)` over a vec, ~0.8 % standard
* error. Handles I64/I32/I16/I8/U8/BOOL/F64/DATE/TIME/TIMESTAMP/SYM/
* STR. Nulls are skipped (matches the SQL `count distinct` semantics).
* Parallelised: each worker builds a private sketch over its row range
* and the main thread merges them before extracting the estimate.
* Wired into `exec_count_distinct` above an input-row threshold. */
ray_t* ;
/* Per-group approximate `count(distinct …)` over a buffered row-index
* layout: group g owns the row indices
* idx_buf[offsets[g] .. offsets[g] + counts[g]).
* Parallelised across groups — each task uses a private stack-resident
* HLL that starts in sparse mode (1 KB) and converts to dense (16 KB)
* on overflow. Sparse mode keeps the memset / estimate cost bounded
* by `min(unique_in_group, sparse_cap)` instead of m, which is the
* decisive win at high group counts where the average group has few
* unique values.
*
* Callers holding a row_gid layout instead build idx_buf+offsets+counts
* once and call this; there's a single per-group kernel. Writes the
* estimate to out[gid]. Returns 0 on success, -1 on unsupported type
* (caller falls back to exact). */
int ;
/* Streaming per-group HLL — single pass over (row_gid[r], hashes[r])
* directly accumulating into n_groups sketches per worker, skipping
* the (idx_buf + offsets + counts) CSR scatter that the _pg_buf entry
* point requires. Each worker owns a private bank of n_groups sparse
* sketches; after the pass, banks are merged element-wise (max) into
* worker 0's bank and the estimates are written to out[gid].
*
* Memory: per worker = n_groups * (sparse_cap*4 + (1<<p)) bytes; at
* p=14 that's ~17 KB per group. Caller must gate on a memory budget
* — this kernel does not validate `n_groups` against available memory.
*
* Supported types: BOOL / U8 / I16 / I32 / I64 / F64 / DATE / TIME /
* TIMESTAMP / SYM. Returns 0 on success, -1 on unsupported type,
* OOM, or empty input. Caller falls back to _pg_buf (which itself
* falls back to exact partitioned dedup) on failure. */
int ;
/* RAY_OPS_HLL_H */