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:
2026-04-14 12:08:13 +09:00
parent 5c067f35a4
commit fe5df22517

View File

@@ -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}