diff --git a/hbrtl/sqlscan.go b/hbrtl/sqlscan.go index ff1be16..15cac38 100644 --- a/hbrtl/sqlscan.go +++ b/hbrtl/sqlscan.go @@ -90,8 +90,27 @@ func SqlScan(t *hbrt.Thread) { return } - // Pre-allocate result: 50k × small-row header pressure matters - rows := make([]hbrt.Value, 0, 1024) + // SQLite-inspired: instead of one slice allocation per row, maintain + // a single flat backing buffer and hand each row a sub-slice into it. + // This halves allocations (row header + backing → just row header) + // and keeps row data contiguous in memory for better cache locality. + // + // Safety: we cap each sub-slice to exactly nFields via the 3-index + // slice form (flat[off:end:end]). Any later `append` on an individual + // row will then trigger a reallocation of that row's backing, so we + // don't clobber neighboring rows if PRG code mutates via AAdd. + // Size the initial backing based on the workarea's record count — + // even if WHERE filters most rows out, over-allocating beats five + // regrowths of a 200 KB buffer mid-scan. + estRows := 1024 + if rc, err := area.RecCount(); err == nil && rc > 0 { + estRows = int(rc) + if estRows > 1 << 20 { + estRows = 1 << 20 + } + } + rows := make([]hbrt.Value, 0, estRows) + flat := make([]hbrt.Value, 0, estRows*nFields) // Scan area.GoTop() @@ -104,8 +123,19 @@ func SqlScan(t *hbrt.Thread) { } if keep { - // Collect column values - row := make([]hbrt.Value, nFields) + // Reserve nFields slots in flat, growing if needed. + off := len(flat) + end := off + nFields + if end > cap(flat) { + // Grow flat. Go's append growth policy handles this; + // we re-reserve space so the sub-slice math still holds. + flat = append(flat, make([]hbrt.Value, nFields)...) + } else { + flat = flat[:end] + } + row := flat[off:end:end] + + // Collect column values directly into the backing buffer. for i := 0; i < nFields; i++ { // GetValue is 0-based v, _ := area.GetValue(fieldPos[i] - 1)