From 3fa553d3ed0187fcbc026ab7a2e37918683ab483 Mon Sep 17 00:00:00 2001 From: Charles KWON OhJun Date: Tue, 7 Apr 2026 19:06:21 +0900 Subject: [PATCH] =?UTF-8?q?perf:=20DBF=20mmap=20=E2=80=94=20zero-copy=20re?= =?UTF-8?q?cord=20reads,=20SCAN=202-3x=20faster?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- hbrdd/dbf/dbf.go | 55 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 48 insertions(+), 7 deletions(-) diff --git a/hbrdd/dbf/dbf.go b/hbrdd/dbf/dbf.go index 01b3ebf..84eb615 100644 --- a/hbrdd/dbf/dbf.go +++ b/hbrdd/dbf/dbf.go @@ -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] = ' ' }