perf(hbrt): ArraySlab — pooled HbArray allocation for scan result rows
SqlScan's prior design called hbrt.MakeArrayFrom per matching row,
each one allocating a fresh &HbArray{}. For 50k rows that's 50k tiny
Go heap allocations + GC pressure that the flat-backing-buffer work
from 85541a3 left untouched (that commit eliminated the per-row items
slice alloc but not the header alloc).
hbrt.ArraySlab pre-allocates a `[]HbArray` slab of the estimated row
count and hands out `&slab.buf[idx]` on each WrapNext. One underlying
make() replaces N; pointers stay stable because slab growth reallocates
a fresh buffer instead of reusing the old one, so previously-handed-out
pointers remain valid (the old backing is kept alive by the references).
API kept tiny:
slab := hbrt.NewArraySlab(estRows)
val := slab.WrapNext(items) // returns Value wrapping &slab.buf[i]
SqlScan now pairs this with the existing flat value buffer for a
single-allocation-per-chunk scan hot loop.
Combined bench impact (50k rows, steady state):
Session start Now
no WHERE 14.6ms 9.2ms ← 1.3x vs raw RDD baseline
numeric WHERE 11.7ms 10.2ms
string WHERE 10.5ms 10.5ms
raw RDD baseline 6.8ms 7.0ms
no WHERE is now within 30% of raw RDD. Remaining gap is largely
Area.GetValue boxing overhead and the pcode opcode dispatch loop
itself — no further structural wins without a wider refactor.
Validation:
- FiveSql2 43/43
- Harbour compat 51/51
- go test ./... ALL PASS
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -279,6 +279,52 @@ func MakeArrayFrom(items []Value) Value {
|
||||
}
|
||||
}
|
||||
|
||||
// ArraySlab is a pre-allocated pool of HbArray headers. SQL scan loops
|
||||
// create one array per matching row; allocating them one at a time
|
||||
// generates O(n) heap traffic. This lets the caller allocate all the
|
||||
// headers in a single backing slice and hand out stable pointers.
|
||||
//
|
||||
// Usage:
|
||||
// slab := hbrt.NewArraySlab(estRows)
|
||||
// for each row:
|
||||
// items := flat[off:end:end]
|
||||
// rows = append(rows, slab.WrapNext(items))
|
||||
//
|
||||
// Each WrapNext advances the slab cursor. If the slab overflows, a new
|
||||
// backing slice is allocated transparently; pointers already handed out
|
||||
// remain valid because they reference fixed addresses in the old slice.
|
||||
type ArraySlab struct {
|
||||
buf []HbArray
|
||||
idx int
|
||||
}
|
||||
|
||||
// NewArraySlab returns an ArraySlab pre-sized for n rows.
|
||||
func NewArraySlab(n int) *ArraySlab {
|
||||
if n < 16 {
|
||||
n = 16
|
||||
}
|
||||
return &ArraySlab{buf: make([]HbArray, n)}
|
||||
}
|
||||
|
||||
// WrapNext stores items into the next free HbArray slot and returns
|
||||
// a Value wrapping it. Grows the slab if exhausted.
|
||||
func (s *ArraySlab) WrapNext(items []Value) Value {
|
||||
if s.idx >= len(s.buf) {
|
||||
// Exhausted — allocate a fresh slab. Old slab stays alive because
|
||||
// previously handed-out pointers reference elements inside it.
|
||||
newSize := len(s.buf) * 2
|
||||
s.buf = make([]HbArray, newSize)
|
||||
s.idx = 0
|
||||
}
|
||||
ha := &s.buf[s.idx]
|
||||
ha.Items = items
|
||||
s.idx++
|
||||
return Value{
|
||||
info: makeInfo(tArray, 0, 0),
|
||||
ptr: unsafe.Pointer(ha),
|
||||
}
|
||||
}
|
||||
|
||||
// MakeObject creates an object Value (array with class).
|
||||
func MakeObject(classID uint16, fieldCount int) Value {
|
||||
ha := &HbArray{Items: make([]Value, fieldCount), Class: classID}
|
||||
|
||||
Reference in New Issue
Block a user