Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 13 additions & 16 deletions db/recsplit/eliasfano16/elias_fano.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func NewEliasFano(count uint64, maxOffset, minDelta uint64) *EliasFano {
func (ef *EliasFano) AddOffset(offset uint64) {
//fmt.Printf("0x%x,\n", offset)
if ef.l != 0 {
setBits(ef.lowerBits, ef.i*ef.l, int(ef.l), (offset-ef.delta)&ef.lowerBitsMask)
setBits(ef.lowerBits, ef.i*ef.l, (offset-ef.delta)&ef.lowerBitsMask)
}
//pos := ((offset - ef.delta) >> ef.l) + ef.i
set(ef.upperBits, ((offset-ef.delta)>>ef.l)+ef.i)
Expand Down Expand Up @@ -332,15 +332,15 @@ func (ef *DoubleEliasFano) Build(cumKeys []uint64, position []uint64) {
for i, cumDelta, bitDelta := uint64(0), uint64(0), uint64(0); i <= ef.numBuckets; i, cumDelta, bitDelta = i+1, cumDelta+ef.cumKeysMinDelta, bitDelta+ef.posMinDelta {
if ef.lCumKeys != 0 {
//fmt.Printf("i=%d, set_bits cum for %d = %b\n", i, cumKeys[i]-cumDelta, (cumKeys[i]-cumDelta)&ef.lowerBitsMaskCumKeys)
setBits(ef.lowerBits, i*(ef.lCumKeys+ef.lPosition), int(ef.lCumKeys), (cumKeys[i]-cumDelta)&ef.lowerBitsMaskCumKeys)
setBits(ef.lowerBits, i*(ef.lCumKeys+ef.lPosition), (cumKeys[i]-cumDelta)&ef.lowerBitsMaskCumKeys)
//fmt.Printf("loweBits %b\n", ef.lowerBits)
}
set(ef.upperBitsCumKeys, ((cumKeys[i]-cumDelta)>>ef.lCumKeys)+i)
//fmt.Printf("i=%d, set cum for %d = %d\n", i, cumKeys[i]-cumDelta, (cumKeys[i]-cumDelta)>>ef.lCumKeys+i)

if ef.lPosition != 0 {
//fmt.Printf("i=%d, set_bits pos for %d = %b\n", i, position[i]-bitDelta, (position[i]-bitDelta)&ef.lowerBitsMaskPosition)
setBits(ef.lowerBits, i*(ef.lCumKeys+ef.lPosition)+ef.lCumKeys, int(ef.lPosition), (position[i]-bitDelta)&ef.lowerBitsMaskPosition)
setBits(ef.lowerBits, i*(ef.lCumKeys+ef.lPosition)+ef.lCumKeys, (position[i]-bitDelta)&ef.lowerBitsMaskPosition)
//fmt.Printf("lowerBits %b\n", ef.lowerBits)
}
set(ef.upperBitsPosition, ((position[i]-bitDelta)>>ef.lPosition)+i)
Expand Down Expand Up @@ -406,19 +406,16 @@ func (ef *DoubleEliasFano) Build(cumKeys []uint64, position []uint64) {
//fmt.Printf("jump: %x\n", ef.jump)
}

// setBits assumes that bits are set in monotonic order, so that
// we can skip the masking for the second word
func setBits(bits []uint64, start uint64, width int, value uint64) {
shift := int(start & 63)
idx64 := start >> 6
mask := (uint64(1)<<width - 1) << shift
//fmt.Printf("mask = %b, idx64 = %d\n", mask, idx64)
bits[idx64] = (bits[idx64] &^ mask) | (value << shift)
//fmt.Printf("start = %d, width = %d, shift + width = %d\n", start, width, shift+width)
if shift+width > 64 {
// changes two 64-bit words
bits[idx64+1] = value >> (64 - shift)
}
// setBits stores a value at bit position start.
// All callers write in monotonic order, so target bits are guaranteed zero
// and we can use |= instead of clear-and-set. The lowerBits slice always
// has +1 padding word, making the unconditional second write safe.
// When shift+width <= 64, value>>(64-shift) == 0, so the write is a no-op.
func setBits(bits []uint64, start uint64, value uint64) {
idx64, shift := start>>6, int(start&63)
_ = bits[idx64+1] // BCE hint: proves both accesses are in-bounds
bits[idx64] |= value << shift
bits[idx64+1] |= value >> (64 - shift)
}

func set(bits []uint64, pos uint64) {
Expand Down
26 changes: 12 additions & 14 deletions db/recsplit/eliasfano32/elias_fano.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func (ef *EliasFano) Size() datasize.ByteSize { return datasize.ByteSize(len(ef.
func (ef *EliasFano) AddOffset(offset uint64) {
//fmt.Printf("0x%x,\n", offset)
if ef.l != 0 {
setBits(ef.lowerBits, ef.i*ef.l, int(ef.l), offset&ef.lowerBitsMask)
setBits(ef.lowerBits, ef.i*ef.l, offset&ef.lowerBitsMask)
}
//pos := ((offset - ef.delta) >> ef.l) + ef.i
set(ef.upperBits, (offset>>ef.l)+ef.i)
Expand Down Expand Up @@ -714,15 +714,15 @@ func (ef *DoubleEliasFano) Build(cumKeys []uint64, position []uint64) {
for i, cumDelta, bitDelta := uint64(0), uint64(0), uint64(0); i <= ef.numBuckets; i, cumDelta, bitDelta = i+1, cumDelta+ef.cumKeysMinDelta, bitDelta+ef.posMinDelta {
if ef.lCumKeys != 0 {
//fmt.Printf("i=%d, set_bits cum for %d = %b\n", i, cumKeys[i]-cumDelta, (cumKeys[i]-cumDelta)&ef.lowerBitsMaskCumKeys)
setBits(ef.lowerBits, i*(ef.lCumKeys+ef.lPosition), int(ef.lCumKeys), (cumKeys[i]-cumDelta)&ef.lowerBitsMaskCumKeys)
setBits(ef.lowerBits, i*(ef.lCumKeys+ef.lPosition), (cumKeys[i]-cumDelta)&ef.lowerBitsMaskCumKeys)
//fmt.Printf("loweBits %b\n", ef.lowerBits)
}
set(ef.upperBitsCumKeys, ((cumKeys[i]-cumDelta)>>ef.lCumKeys)+i)
//fmt.Printf("i=%d, set cum for %d = %d\n", i, cumKeys[i]-cumDelta, (cumKeys[i]-cumDelta)>>ef.lCumKeys+i)

if ef.lPosition != 0 {
//fmt.Printf("i=%d, set_bits pos for %d = %b\n", i, position[i]-bitDelta, (position[i]-bitDelta)&ef.lowerBitsMaskPosition)
setBits(ef.lowerBits, i*(ef.lCumKeys+ef.lPosition)+ef.lCumKeys, int(ef.lPosition), (position[i]-bitDelta)&ef.lowerBitsMaskPosition)
setBits(ef.lowerBits, i*(ef.lCumKeys+ef.lPosition)+ef.lCumKeys, (position[i]-bitDelta)&ef.lowerBitsMaskPosition)
//fmt.Printf("lowerBits %b\n", ef.lowerBits)
}
set(ef.upperBitsPosition, ((position[i]-bitDelta)>>ef.lPosition)+i)
Expand Down Expand Up @@ -788,18 +788,16 @@ func (ef *DoubleEliasFano) Build(cumKeys []uint64, position []uint64) {
//fmt.Printf("jump: %x\n", ef.jump)
}

// setBits assumes that bits are set in monotonic order, so that
// we can skip the masking for the second word
func setBits(bits []uint64, start uint64, width int, value uint64) {
// setBits stores a value at bit position start.
// All callers write in monotonic order, so target bits are guaranteed zero
// and we can use |= instead of clear-and-set. The lowerBits slice always
// has +1 padding word, making the unconditional second write safe.
// When shift+width <= 64, value>>(64-shift) == 0, so the write is a no-op.
func setBits(bits []uint64, start uint64, value uint64) {
idx64, shift := start>>6, int(start&63)
mask := (uint64(1)<<width - 1) << shift
//fmt.Printf("mask = %b, idx64 = %d\n", mask, idx64)
bits[idx64] = (bits[idx64] &^ mask) | (value << shift)
//fmt.Printf("start = %d, width = %d, shift + width = %d\n", start, width, shift+width)
if shift+width > 64 {
// changes two 64-bit words
bits[idx64+1] = value >> (64 - shift)
}
_ = bits[idx64+1] // BCE hint: proves both accesses are in-bounds
bits[idx64] |= value << shift
bits[idx64+1] |= value >> (64 - shift)
}

func set(bits []uint64, pos uint64) {
Expand Down
29 changes: 29 additions & 0 deletions db/recsplit/eliasfano32/elias_fano_seek_bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,35 @@ func BenchmarkSeek(b *testing.B) {
}
}

// BenchmarkAddOffset measures the build path (AddOffset + Build) cost.
func BenchmarkAddOffset(b *testing.B) {
const count = 1_000_000

cases := []struct {
name string
stride uint64
}{
{"stride1_l0", 1},
{"stride123_l6", 123},
{"stride1000_l9", 1000},
}

for _, tc := range cases {
tc := tc
b.Run(tc.name, func(b *testing.B) {
b.ReportAllocs()
maxOffset := (count - 1) * tc.stride
for b.Loop() {
ef := NewEliasFano(count, maxOffset)
for i := uint64(0); i < count; i++ {
ef.AddOffset(i * tc.stride)
}
ef.Build()
}
})
}
}

// BenchmarkSeekPool models real mainnet seek patterns:
// - a pool of many small EFs placed at random offsets in a large global range
// (matching the real distribution: 83% of EFs have 1–3 word upperBits = 8–24 bytes)
Expand Down
Loading