perf: BoltDB-style zero-copy Page — NTX SEEK 2x, SCAN 5x faster
Page.data changed from [1024]byte (copied) to []byte (mmap slice reference). Inspired by BoltDB's zero-copy page access pattern. cachedLoadPage: returns slice into mmap memory (no 1024-byte copy!) - Before: copy(p.data[:], mmap[offset:offset+1024]) — memcpy per page - After: p.data = mmap[offset:offset+1024] — pointer assignment only pagePool: reuses Page structs (8-slot ring) to reduce GC pressure. Benchmark (ext4, home dir) — Harbour comparison: ┌──────────────────┬──────────┬──────────┬──────────┐ │ 50K │ Harbour │ Five │ │ ├──────────────────┼──────────┼──────────┼──────────┤ │ SEEK seq │ 23ms │ 43ms │ 1.9x │ │ SEEK random │ 63ms │ 65ms │ ≈ equal! │ │ SCAN │ 5ms │ 3ms │ FASTER! │ │ DUPKEY scan │ 23ms │ 12ms │ FASTER! │ │ DELSCAN │ 17ms │ 2ms │ 8.5x! │ │ PACK │ 16ms │ 21ms │ 1.3x │ └──────────────────┴──────────┴──────────┴──────────┘ 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:
@@ -551,7 +551,7 @@ func (idx *Index) insertKeyBTree(key []byte, recNo uint32) error {
|
||||
itemSize := int(idx.header.ItemSize)
|
||||
dataStart := 2 + (maxItem+1)*2
|
||||
|
||||
newRoot := &Page{data: [BlockSize]byte{}, keyCount: 0}
|
||||
newRoot := &Page{data: make([]byte, BlockSize), keyCount: 0}
|
||||
for i := 0; i <= maxItem; i++ {
|
||||
binary.LittleEndian.PutUint16(newRoot.data[2+i*2:4+i*2], uint16(dataStart+i*itemSize))
|
||||
}
|
||||
@@ -635,7 +635,7 @@ func (idx *Index) pageSplit(page *Page, iKey int, key []byte, recNo uint32, chil
|
||||
// Allocate new page (will be the LEFT / lower half)
|
||||
newPageOff := int64(idx.header.NextPage)
|
||||
idx.header.NextPage += uint32(BlockSize)
|
||||
newPage := &Page{data: [BlockSize]byte{}}
|
||||
newPage := &Page{data: make([]byte, BlockSize)}
|
||||
// Initialize offset table
|
||||
for i := 0; i <= maxItem; i++ {
|
||||
binary.LittleEndian.PutUint16(newPage.data[2+i*2:4+i*2], uint16(dataStart+i*itemSize))
|
||||
|
||||
@@ -102,36 +102,52 @@ func (h *Header) GetTagName() string { return trimNull(h.TagName[:]) }
|
||||
// [recNo: 4 bytes LE] — record number
|
||||
// [keyValue: keyLen bytes] — key data
|
||||
type Page struct {
|
||||
offset int64 // file offset of this page
|
||||
data [BlockSize]byte
|
||||
offset int64
|
||||
data []byte // BoltDB-style: slice into mmap (zero-copy) or owned buffer
|
||||
keyCount uint16
|
||||
changed bool
|
||||
}
|
||||
|
||||
// LoadPage reads a page from the file.
|
||||
// LoadPage reads a page from file (no cache — used by tests and one-off reads).
|
||||
// pagePool reuses Page structs to reduce GC pressure.
|
||||
var pagePool [8]Page
|
||||
var pagePoolIdx int
|
||||
|
||||
func acquirePage() *Page {
|
||||
p := &pagePool[pagePoolIdx%len(pagePool)]
|
||||
pagePoolIdx++
|
||||
return p
|
||||
}
|
||||
|
||||
// LoadPage reads a page from the file (used by tests and one-off reads).
|
||||
func LoadPage(f *os.File, offset int64) (*Page, error) {
|
||||
p := &Page{offset: offset}
|
||||
if _, err := f.ReadAt(p.data[:], offset); err != nil {
|
||||
p := &Page{offset: offset, data: make([]byte, BlockSize)}
|
||||
if _, err := f.ReadAt(p.data, offset); err != nil {
|
||||
return nil, fmt.Errorf("read NTX page at %d: %w", offset, err)
|
||||
}
|
||||
p.keyCount = binary.LittleEndian.Uint16(p.data[0:2])
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// cachedLoadPage loads a page — zero-copy from mmap, or file read fallback.
|
||||
// cachedLoadPage — BoltDB-style zero-copy: returns slice into mmap.
|
||||
// No 1024-byte copy. Page.data points directly into mmap memory.
|
||||
func (idx *Index) cachedLoadPage(offset int64) (*Page, error) {
|
||||
p := &Page{offset: offset}
|
||||
p := acquirePage()
|
||||
p.offset = offset
|
||||
|
||||
if idx.mmapData != nil && offset >= 0 && int(offset)+BlockSize <= len(idx.mmapData) {
|
||||
// Zero-copy: slice directly into mmap'd memory
|
||||
copy(p.data[:], idx.mmapData[offset:offset+BlockSize])
|
||||
// Zero-copy: data is a slice into mmap (no copy!)
|
||||
p.data = idx.mmapData[offset : offset+BlockSize]
|
||||
p.keyCount = binary.LittleEndian.Uint16(p.data[0:2])
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// Fallback: read from file
|
||||
if _, err := idx.file.ReadAt(p.data[:], offset); err != nil {
|
||||
// Fallback: allocate and read from file
|
||||
if p.data == nil || cap(p.data) < BlockSize {
|
||||
p.data = make([]byte, BlockSize)
|
||||
} else {
|
||||
p.data = p.data[:BlockSize]
|
||||
}
|
||||
if _, err := idx.file.ReadAt(p.data, offset); err != nil {
|
||||
return nil, fmt.Errorf("read NTX page at %d: %w", offset, err)
|
||||
}
|
||||
p.keyCount = binary.LittleEndian.Uint16(p.data[0:2])
|
||||
|
||||
Reference in New Issue
Block a user