perf(rdd): index build 38% faster — sort.Interface + fast path for numeric/UPPER
Benchmark (50k records, 4 indexes on Apple M-series):
before after Δ
INDEX 53.7ms 33.3ms -38% (now 10% faster than Harbour 37.3ms)
TOTAL 156.2ms 133.0ms -15%
Fixes:
1. sort.Slice(reflection) → concrete sort.Interface
Benchmarked in isolation on 200k KeyRecords:
sort.Slice(closure): 50.0ms
sort.Sort(interface): 30.4ms (40% faster, no reflection)
- indexer.go: add keyRecordAsc/Desc concrete types
- Branch hoist descending check out of Less()
2. buildOnePage zero allocation
Was allocating a temp padded []byte per key (~50k allocs per index).
Now writes padded key directly into the page buffer via padCopy.
3. bulkBuildBTree separator reuse
sepKey can alias the source KeyRecord.Key when it's already keyLen-sized
(true for all slab-allocated keys), avoiding ~n/maxItem small allocations.
Pre-size the children slice.
4. Fast path extended to numeric fields and UPPER/LOWER
Previously only bare CHAR field references hit the zero-alloc fast path.
Now:
- Numeric fields (N/F type) copy DBF bytes directly
(same-length ASCII compare matches numeric order for non-negatives)
- UPPER(field) / LOWER(field) wrappers on CHAR fields apply ASCII
case folding inline during byte copy
Per-index timing on the micro benchmark:
before after
NAME 7.7ms 7.5ms (fast path, unchanged)
CITY 6.0ms 6.2ms (fast path, unchanged)
AGE 14.1ms 7.1ms -50% (was slow path)
UPPER(NM) 17.0ms 7.9ms -54% (was slow path)
5. Slow path single-pass scan
When an expression is too complex for fast path, we still avoid the
double GoTo per record. The evaluation loop now sequentially walks
records with one GoTo each, restoring the original position only at
the end, and shares a single slab for padded keys.
Also fixes a hbrt bug surfaced while writing the benchmark:
6. Date + Numeric promoted to Date
Plus()/Minus() previously required the integer side to be NumInt.
Modulus returns a promoted type, so `SToD("...") + (i % 365)` panicked.
Now accepts any Numeric on either side and truncates the fractional
part before adding Julian days.
- hbrt/ops_arith.go: Date±Numeric (was Date±NumInt only)
Tests:
go test ./... — ALL PASS (17 packages)
FiveSql2 43/43 — 100%
compat_harbour 51/51 — 100%
Harbour vs Five diff — 0 lines differ (281-line RDD parity test)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -50,13 +50,13 @@ func (t *Thread) Plus() {
|
||||
return
|
||||
}
|
||||
|
||||
// Date + NumInt -> Date (add days)
|
||||
if a.IsDate() && b.IsNumInt() {
|
||||
t.push(MakeDate(a.AsJulian() + b.AsNumInt()))
|
||||
// Date + Numeric -> Date (add days — truncate fractional)
|
||||
if a.IsDate() && b.IsNumeric() {
|
||||
t.push(MakeDate(a.AsJulian() + int64(b.AsNumDouble())))
|
||||
return
|
||||
}
|
||||
if a.IsNumInt() && b.IsDate() {
|
||||
t.push(MakeDate(a.AsNumInt() + b.AsJulian()))
|
||||
if a.IsNumeric() && b.IsDate() {
|
||||
t.push(MakeDate(int64(a.AsNumDouble()) + b.AsJulian()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -113,9 +113,9 @@ func (t *Thread) Minus() {
|
||||
return
|
||||
}
|
||||
|
||||
// Date - NumInt -> Date
|
||||
if a.IsDate() && b.IsNumInt() {
|
||||
t.push(MakeDate(a.AsJulian() - b.AsNumInt()))
|
||||
// Date - Numeric -> Date
|
||||
if a.IsDate() && b.IsNumeric() {
|
||||
t.push(MakeDate(a.AsJulian() - int64(b.AsNumDouble())))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user