perf: CDX zero-alloc internal node seek — SEEK 45% faster

Internal node traversal: read directly from mmap/buf slice
- No DecodeIntKeys allocation (was nKeys+1 IntKeyEntry structs)
- No key byte slice copy (compare directly against buf)
- Big-endian child/recNo read inline

CDX 50K benchmark:
  SEEK NAME: 362ms → 199ms (45% faster)
  SEEK ID:   320ms → 184ms (42% faster)
  SCAN:      14ms (unchanged — leaf cache handles this)
  SCOPE:     20ms → 14ms

Harbour comparison:
  SEEK: 27ms (Harbour) vs 199ms (Five) = 7.4x
  SCAN: 6ms (Harbour) vs 14ms (Five) = 2.3x

CDX cross-read: 18/18 PASS.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-07 14:05:20 +09:00
parent 40935b6103
commit 96d72a456c

View File

@@ -653,27 +653,37 @@ func (t *Tag) seekPage(pageOffset int64, searchKey []byte) (uint32, bool) {
return 0, false
}
// Internal node: binary search then follow child
node := DecodeIntNode(buf)
intKeys := DecodeIntKeys(buf, int(node.NKeys), t.keyLen)
// Internal node: binary search directly on raw page data (zero allocation)
// CDX internal format: [12-byte header][key:keyLen][recNo:4BE][child:4BE]...
nKeys := int(binary.LittleEndian.Uint16(buf[2:4]))
entrySize := t.keyLen + 8
if t.stackLevel < StackSize {
t.stack[t.stackLevel] = StackEntry{PageOffset: pageOffset, KeyIndex: 0}
t.stackLevel++
}
for i := 0; i < int(node.NKeys); i++ {
cmp := bytes.Compare(searchKey, intKeys[i].Key)
for i := 0; i < nKeys; i++ {
off := IntHeadSize + i*entrySize
cmp := bytes.Compare(searchKey, buf[off:off+t.keyLen])
if cmp <= 0 {
childPage := binary.BigEndian.Uint32(buf[off+t.keyLen+4 : off+t.keyLen+8])
t.stack[t.stackLevel-1].KeyIndex = i
return t.seekPage(int64(intKeys[i].ChildPage), searchKey)
return t.seekPage(int64(childPage), searchKey)
}
}
// Follow last child
lastIdx := int(node.NKeys)
t.stack[t.stackLevel-1].KeyIndex = lastIdx
return t.seekPage(int64(intKeys[lastIdx].ChildPage), searchKey)
// Follow rightmost child (entry[nKeys] — trailing child pointer)
trailOff := IntHeadSize + nKeys*entrySize
t.stack[t.stackLevel-1].KeyIndex = nKeys
if trailOff+t.keyLen+8 <= PageLen {
trailChild := binary.BigEndian.Uint32(buf[trailOff+t.keyLen+4 : trailOff+t.keyLen+8])
if trailChild != 0 {
return t.seekPage(int64(trailChild), searchKey)
}
}
t.tagEOF = true
return 0, false
}
// GoTop positions at the first key.
@@ -710,15 +720,16 @@ func (t *Tag) goLeftmost(pageOffset int64) bool {
return false
}
// Internal: follow first child
node := DecodeIntNode(buf)
intKeys := DecodeIntKeys(buf, int(node.NKeys), t.keyLen)
if len(intKeys) > 0 {
// Internal: follow first child (zero allocation — read directly from buf)
nKeys := int(binary.LittleEndian.Uint16(buf[2:4]))
if nKeys > 0 {
entrySize := t.keyLen + 8
child0 := binary.BigEndian.Uint32(buf[IntHeadSize+t.keyLen+4 : IntHeadSize+entrySize])
if t.stackLevel < StackSize {
t.stack[t.stackLevel] = StackEntry{PageOffset: pageOffset, KeyIndex: 0}
t.stackLevel++
}
return t.goLeftmost(int64(intKeys[0].ChildPage))
return t.goLeftmost(int64(child0))
}
return false
}