perf: DBF mmap — zero-copy record reads, SCAN 2-3x faster

syscall.Mmap on DBF open for zero-copy GoTo:
- GoTo: copy from mmap slice instead of file.ReadAt syscall
- Eliminates kernel context switch per record read
- Unmap before Append (file grows), Close, Flush
- Re-mmap after Flush

Benchmark (ext4, home dir):
  10K SCAN: 4ms → 2ms (Harbour 1ms = 2x)
  10K DUPKEY: 6ms → 4ms (Harbour 4ms = 1x!)
  10K DELSCAN: 6ms → 2ms (Harbour 2ms = 1x!)
  50K SCAN: 26ms → 15ms (42% faster)
  50K DELSCAN: 29ms → 17ms (Harbour 17ms = 1x!)
  50K DUPKEY: 41ms → 29ms (Harbour 23ms = 1.3x)
  CDX SCAN: 13ms → 4ms (Harbour 6ms — FASTER!)
  CDX SCOPE: 9ms → 3ms (Harbour 4ms — FASTER!)

82/82 stress PASS. All unit tests PASS.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-07 19:06:21 +09:00
parent 3974333372
commit 3fa553d3ed

View File

@@ -18,6 +18,7 @@ import (
"fmt"
"os"
"strings"
"syscall"
)
// DBFArea implements the DBF database driver.
@@ -46,6 +47,9 @@ type DBFArea struct {
ghost bool // at phantom record (after APPEND)
recLoaded bool // false = recBuf stale, need loadRecord()
// mmap for zero-copy record reads
mmapData []byte
// Memo file (FPT)
memoFile *FPTFile
@@ -211,7 +215,10 @@ func openDBF(drv *DBFDriver, params hbrdd.OpenParams) (*DBFArea, error) {
// If FPT doesn't exist, memo reads return empty string
}
// Step 9: Position at first record
// Step 9: mmap DBF for zero-copy record reads
area.mmapDBF()
// Step 10: Position at first record
area.FEof = (area.recCount == 0)
if area.recCount > 0 {
area.GoTo(1)
@@ -313,10 +320,10 @@ func createDBF(drv *DBFDriver, params hbrdd.CreateParams) (*DBFArea, error) {
func (a *DBFArea) Driver() hbrdd.Driver { return &DBFDriver{} }
func (a *DBFArea) Close() error {
a.unmapDBF() // unmap before writing
if a.dirty {
a.flushRecord()
}
// Write EOF + header once on close
a.dataFile.WriteAt([]byte{EOFMarker}, a.header.EOFOffset())
a.updateHeader()
if a.memoFile != nil {
@@ -337,9 +344,12 @@ func (a *DBFArea) Flush() error {
return err
}
}
a.unmapDBF()
a.dataFile.WriteAt([]byte{EOFMarker}, a.header.EOFOffset())
a.updateHeader()
return a.dataFile.Sync()
err := a.dataFile.Sync()
a.mmapDBF() // re-mmap after flush
return err
}
// --- Movement ---
@@ -386,9 +396,14 @@ func (a *DBFArea) GoTo(recNo uint32) error {
return nil
}
// Read record from file
// Read record — mmap fast path or file fallback
offset := a.header.RecordOffset(recNo)
a.dataFile.ReadAt(a.recBuf, offset)
recLen := int(a.header.RecordLen)
if a.mmapData != nil && int(offset)+recLen <= len(a.mmapData) {
copy(a.recBuf, a.mmapData[offset:offset+int64(recLen)])
} else {
a.dataFile.ReadAt(a.recBuf, offset)
}
a.recNo = recNo
a.recLoaded = true
@@ -507,7 +522,29 @@ func (a *DBFArea) Skip(count int64) error {
return nil
}
// loadRecord reads the current record from disk if not already loaded.
// mmapDBF maps the DBF file for zero-copy reads. Called after open.
func (a *DBFArea) mmapDBF() {
fi, err := a.dataFile.Stat()
if err != nil || fi.Size() < int64(a.header.HeaderLen) {
return
}
data, err := syscall.Mmap(int(a.dataFile.Fd()), 0, int(fi.Size()),
syscall.PROT_READ, syscall.MAP_SHARED)
if err != nil {
return
}
a.mmapData = data
}
// unmapDBF releases the mmap.
func (a *DBFArea) unmapDBF() {
if a.mmapData != nil {
syscall.Munmap(a.mmapData)
a.mmapData = nil
}
}
// loadRecord reads the current record — from mmap or file fallback.
func (a *DBFArea) loadRecord() {
if a.recLoaded || a.FEof || a.recNo == 0 || a.recNo > a.recCount {
return
@@ -582,11 +619,15 @@ func (a *DBFArea) Append() error {
a.flushRecord()
}
// Unmap — file will grow
if a.mmapData != nil {
a.unmapDBF()
}
a.recCount++
a.recNo = a.recCount
a.header.RecCount = a.recCount
// Clear record buffer (all spaces)
for i := range a.recBuf {
a.recBuf[i] = ' '
}