treap, ffldb: change Put() to take in multiple key-value pairs

Previously, the Put() function took in a single key-value pair,
resulting in memory allocation overhead as the intermediary treapNodes
got allocated and immediately garbage collected, resulting in
performance loss due to the garbage collection overhead.

We're able to recycle the intermediary treapNodes allocated by taking in
multiple key-value pairs.  This results in better performance of the
node especially during UTXO cache flushes.
This commit is contained in:
Calvin Kim 2025-09-20 20:38:35 +09:00 committed by Calvin Kim
parent 0e95ae381a
commit e009124748
4 changed files with 213 additions and 89 deletions

View file

@ -611,19 +611,25 @@ func (c *dbCache) commitTx(tx *transaction) error {
c.cacheLock.RUnlock()
// Apply every key to add in the database transaction to the cache.
pendingKVs := make([]treap.KVPair, 0, tx.pendingKeys.Len())
tx.pendingKeys.ForEach(func(k, v []byte) bool {
pendingKVs = append(pendingKVs, treap.KVPair{Key: k, Value: v})
newCachedRemove = newCachedRemove.Delete(k)
newCachedKeys = newCachedKeys.Put(k, v)
return true
})
newCachedKeys = newCachedKeys.Put(pendingKVs...)
tx.pendingKeys = nil
// Apply every key to remove in the database transaction to the cache.
pendingRemoveKVs := make([]treap.KVPair, 0, tx.pendingRemove.Len())
tx.pendingRemove.ForEach(func(k, v []byte) bool {
pendingRemoveKVs = append(pendingRemoveKVs, treap.KVPair{Key: k, Value: v})
newCachedKeys = newCachedKeys.Delete(k)
newCachedRemove = newCachedRemove.Put(k, nil)
return true
})
newCachedRemove = newCachedRemove.Put(pendingRemoveKVs...)
tx.pendingRemove = nil
// Atomically replace the immutable treaps which hold the cached keys to

View file

@ -105,10 +105,52 @@ func (t *Immutable) Get(key []byte) []byte {
return nil
}
// Put inserts the passed key/value pair.
func (t *Immutable) Put(key, value []byte) *Immutable {
immutable, _ := t.put(key, value)
return immutable
// KVPair is just a helper struct for a key-value pair that's going to be
// inserted into the treap.
type KVPair struct {
Key []byte
Value []byte
}
// Put puts the passed in key/value pairs into the treap. For operations
// requiring many insertions at once, Put is memory efficient as the
// intermediary treap nodes created between each put operation is recycled
// through an internal sync.Pool, reducing overall memory allocation.
func (t *Immutable) Put(kvPairs ...KVPair) *Immutable {
treap := t
var prevTreapNodes [staticDepth]*treapNode
for _, kvPair := range kvPairs {
newTreap, newTreapNodes := treap.put(kvPair.Key, kvPair.Value)
// Loop through the prevTreapNodes and check for treapNodes that
// are no longer being utilized. These will be garbaged collected
// and they're better off being recycled in the treapNodePool.
for _, node := range prevTreapNodes {
if node == nil {
break
}
// Make sure that the node we're going to recycle isn't
// being used by the latest immutable treap by checking
// if the pointer value of the node is the same.
got := newTreap.get(node.key)
if got == node {
continue
}
// This node is only being used by the previous immutable
// copy and can safely be put into the treapNodePool to be
// recycled.
node.recycle()
}
// Replace with the latest treap and treap nodes.
treap = newTreap
prevTreapNodes = newTreapNodes
}
return treap
}
// put inserts the passed key/value pair and returns all the newly created

View file

@ -61,31 +61,43 @@ func TestImmutableSequential(t *testing.T) {
// functions work as expected.
expectedSize := uint64(0)
numItems := 1000
keyCount := 100
testTreap := NewImmutable()
for i := 0; i < numItems; i++ {
key := serializeUint32(uint32(i))
testTreap = testTreap.Put(key, key)
for i := 0; i < numItems/keyCount; i++ {
keys := make([][]byte, 0, keyCount)
kvPairs := make([]KVPair, 0, keyCount)
for j := 0; j < keyCount; j++ {
n := i*keyCount + j
key := serializeUint32(uint32(n))
keys = append(keys, key)
kvPairs = append(kvPairs, KVPair{key, key})
}
testTreap = testTreap.Put(kvPairs...)
// Ensure the treap length is the expected value.
if gotLen := testTreap.Len(); gotLen != i+1 {
if gotLen := testTreap.Len(); gotLen != (i+1)*keyCount {
t.Fatalf("Len #%d: unexpected length - got %d, want %d",
i, gotLen, i+1)
}
// Ensure the treap has the key.
if !testTreap.Has(key) {
t.Fatalf("Has #%d: key %q is not in treap", i, key)
}
for j, key := range keys {
// Ensure the treap has the key.
if !testTreap.Has(key) {
t.Fatalf("Has #%d#%d: key %q is not in treap", i, j, key)
}
// Get the key from the treap and ensure it is the expected
// value.
if gotVal := testTreap.Get(key); !bytes.Equal(gotVal, key) {
t.Fatalf("Get #%d: unexpected value - got %x, want %x",
i, gotVal, key)
// Get the key from the treap and ensure it is the expected
// value.
if gotVal := testTreap.Get(key); !bytes.Equal(gotVal, key) {
t.Fatalf("Get #%d#%d: unexpected value - got %x, want %x",
i, j, gotVal, key)
}
expectedSize += (nodeFieldsSize + 8)
}
// Ensure the expected size is reported.
expectedSize += (nodeFieldsSize + 8)
if gotSize := testTreap.Size(); gotSize != expectedSize {
t.Fatalf("Size #%d: unexpected byte size - got %d, "+
"want %d", i, gotSize, expectedSize)
@ -161,31 +173,43 @@ func TestImmutableReverseSequential(t *testing.T) {
// functions work as expected.
expectedSize := uint64(0)
numItems := 1000
keyCount := 100
testTreap := NewImmutable()
for i := 0; i < numItems; i++ {
key := serializeUint32(uint32(numItems - i - 1))
testTreap = testTreap.Put(key, key)
for i := 0; i < numItems/keyCount; i++ {
keys := make([][]byte, 0, keyCount)
kvPairs := make([]KVPair, 0, keyCount)
for j := 0; j < keyCount; j++ {
n := numItems - (i * keyCount) - j - 1
key := serializeUint32(uint32(n))
keys = append(keys, key)
kvPairs = append(kvPairs, KVPair{key, key})
}
testTreap = testTreap.Put(kvPairs...)
// Ensure the treap length is the expected value.
if gotLen := testTreap.Len(); gotLen != i+1 {
if gotLen := testTreap.Len(); gotLen != (i+1)*keyCount {
t.Fatalf("Len #%d: unexpected length - got %d, want %d",
i, gotLen, i+1)
}
// Ensure the treap has the key.
if !testTreap.Has(key) {
t.Fatalf("Has #%d: key %q is not in treap", i, key)
}
for j, key := range keys {
// Ensure the treap has the key.
if !testTreap.Has(key) {
t.Fatalf("Has #%d#%d: key %q is not in treap", i, j, key)
}
// Get the key from the treap and ensure it is the expected
// value.
if gotVal := testTreap.Get(key); !bytes.Equal(gotVal, key) {
t.Fatalf("Get #%d: unexpected value - got %x, want %x",
i, gotVal, key)
// Get the key from the treap and ensure it is the expected
// value.
if gotVal := testTreap.Get(key); !bytes.Equal(gotVal, key) {
t.Fatalf("Get #%d#%d: unexpected value - got %x, want %x",
i, j, gotVal, key)
}
expectedSize += (nodeFieldsSize + 8)
}
// Ensure the expected size is reported.
expectedSize += (nodeFieldsSize + 8)
if gotSize := testTreap.Size(); gotSize != expectedSize {
t.Fatalf("Size #%d: unexpected byte size - got %d, "+
"want %d", i, gotSize, expectedSize)
@ -262,33 +286,45 @@ func TestImmutableUnordered(t *testing.T) {
// treap functions work as expected.
expectedSize := uint64(0)
numItems := 1000
keyCount := 100
testTreap := NewImmutable()
for i := 0; i < numItems; i++ {
for i := 0; i < numItems/keyCount; i++ {
// Hash the serialized int to generate out-of-order keys.
hash := sha256.Sum256(serializeUint32(uint32(i)))
key := hash[:]
testTreap = testTreap.Put(key, key)
keys := make([][]byte, 0, keyCount)
kvPairs := make([]KVPair, 0, keyCount)
for j := 0; j < keyCount; j++ {
n := i*keyCount + j
hash := sha256.Sum256(serializeUint32(uint32(n)))
key := hash[:]
keys = append(keys, key)
kvPairs = append(kvPairs, KVPair{key, key})
}
testTreap = testTreap.Put(kvPairs...)
// Ensure the treap length is the expected value.
if gotLen := testTreap.Len(); gotLen != i+1 {
if gotLen := testTreap.Len(); gotLen != (i+1)*keyCount {
t.Fatalf("Len #%d: unexpected length - got %d, want %d",
i, gotLen, i+1)
}
// Ensure the treap has the key.
if !testTreap.Has(key) {
t.Fatalf("Has #%d: key %q is not in treap", i, key)
}
for j, key := range keys {
// Ensure the treap has the key.
if !testTreap.Has(key) {
t.Fatalf("Has #%d#%d: key %q is not in treap", i, j, key)
}
// Get the key from the treap and ensure it is the expected
// value.
if gotVal := testTreap.Get(key); !bytes.Equal(gotVal, key) {
t.Fatalf("Get #%d: unexpected value - got %x, want %x",
i, gotVal, key)
// Get the key from the treap and ensure it is the expected
// value.
if gotVal := testTreap.Get(key); !bytes.Equal(gotVal, key) {
t.Fatalf("Get #%d#%d: unexpected value - got %x, want %x",
i, j, gotVal, key)
}
expectedSize += nodeFieldsSize + uint64(len(key)+len(key))
}
// Ensure the expected size is reported.
expectedSize += nodeFieldsSize + uint64(len(key)+len(key))
if gotSize := testTreap.Size(); gotSize != expectedSize {
t.Fatalf("Size #%d: unexpected byte size - got %d, "+
"want %d", i, gotSize, expectedSize)
@ -335,31 +371,53 @@ func TestImmutableUnordered(t *testing.T) {
func TestImmutableDuplicatePut(t *testing.T) {
t.Parallel()
keyCount := 100
expectedVal := []byte("testval")
expectedSize := uint64(0)
numItems := 1000
testTreap := NewImmutable()
for i := 0; i < numItems; i++ {
key := serializeUint32(uint32(i))
testTreap = testTreap.Put(key, key)
expectedSize += nodeFieldsSize + uint64(len(key)+len(key))
// Put a duplicate key with the expected final value.
testTreap = testTreap.Put(key, expectedVal)
// Ensure the key still exists and is the new value.
if gotVal := testTreap.Has(key); !gotVal {
t.Fatalf("Has: unexpected result - got %v, want true",
gotVal)
for i := 0; i < numItems/keyCount; i++ {
keys := make([][]byte, 0, keyCount)
kvPairs := make([]KVPair, 0, keyCount)
for j := 0; j < keyCount; j++ {
n := i*keyCount + j
key := serializeUint32(uint32(n))
keys = append(keys, key)
kvPairs = append(kvPairs, KVPair{key, key})
}
if gotVal := testTreap.Get(key); !bytes.Equal(gotVal, expectedVal) {
t.Fatalf("Get: unexpected result - got %x, want %x",
gotVal, expectedVal)
testTreap = testTreap.Put(kvPairs...)
// Get expectedSize.
for _, key := range keys {
expectedSize += nodeFieldsSize + uint64(len(key)+len(key))
}
// Put duplicate keys with the expected final values.
expectedPairs := make([]KVPair, keyCount)
for i := range expectedPairs {
expectedPairs[i] = KVPair{keys[i], expectedVal}
}
testTreap = testTreap.Put(expectedPairs...)
// Ensure the keys still exist and is the new value.
for _, key := range keys {
if gotVal := testTreap.Has(key); !gotVal {
t.Fatalf("Has: unexpected result - got %v, want true",
gotVal)
}
if gotVal := testTreap.Get(key); !bytes.Equal(gotVal, expectedVal) {
t.Fatalf("Get: unexpected result - got %x, want %x",
gotVal, expectedVal)
}
expectedSize -= uint64(len(key))
expectedSize += uint64(len(expectedVal))
}
// Ensure the expected size is reported.
expectedSize -= uint64(len(key))
expectedSize += uint64(len(expectedVal))
if gotSize := testTreap.Size(); gotSize != expectedSize {
t.Fatalf("Size: unexpected byte size - got %d, want %d",
gotSize, expectedSize)
@ -376,7 +434,7 @@ func TestImmutableNilValue(t *testing.T) {
// Put the key with a nil value.
testTreap := NewImmutable()
testTreap = testTreap.Put(key, nil)
testTreap = testTreap.Put(KVPair{key, nil})
// Ensure the key exists and is an empty byte slice.
if gotVal := testTreap.Has(key); !gotVal {
@ -399,10 +457,12 @@ func TestImmutableForEachStopIterator(t *testing.T) {
// Insert a few keys.
numItems := 10
testTreap := NewImmutable()
kvPairs := make([]KVPair, 0, numItems)
for i := 0; i < numItems; i++ {
key := serializeUint32(uint32(i))
testTreap = testTreap.Put(key, key)
kvPairs = append(kvPairs, KVPair{key, key})
}
testTreap = testTreap.Put(kvPairs...)
// Ensure ForEach exits early on false return by caller.
var numIterated int
@ -426,38 +486,50 @@ func TestImmutableSnapshot(t *testing.T) {
// functions work as expected.
expectedSize := uint64(0)
numItems := 1000
keyCount := 100
testTreap := NewImmutable()
for i := 0; i < numItems; i++ {
for i := 0; i < numItems/keyCount; i++ {
treapSnap := testTreap
key := serializeUint32(uint32(i))
testTreap = testTreap.Put(key, key)
keys := make([][]byte, 0, keyCount)
kvPairs := make([]KVPair, 0, keyCount)
for j := 0; j < keyCount; j++ {
n := i*keyCount + j
key := serializeUint32(uint32(n))
keys = append(keys, key)
kvPairs = append(kvPairs, KVPair{key, key})
}
testTreap = testTreap.Put(kvPairs...)
// Ensure the length of the treap snapshot is the expected
// value.
if gotLen := treapSnap.Len(); gotLen != i {
if gotLen := treapSnap.Len(); gotLen != i*keyCount {
t.Fatalf("Len #%d: unexpected length - got %d, want %d",
i, gotLen, i)
}
// Ensure the treap snapshot does not have the key.
if treapSnap.Has(key) {
t.Fatalf("Has #%d: key %q is in treap", i, key)
for j, key := range keys {
// Ensure the treap snapshot does not have the key.
if treapSnap.Has(key) {
t.Fatalf("Has #%d#%d: key %q is in treap", i, j, key)
}
// Get the key that doesn't exist in the treap snapshot and
// ensure it is nil.
if gotVal := treapSnap.Get(key); gotVal != nil {
t.Fatalf("Get #%d#%d: unexpected value - got %x, want nil",
i, j, gotVal)
}
// Ensure the expected size is reported.
if gotSize := treapSnap.Size(); gotSize != expectedSize {
t.Fatalf("Size #%d#%d: unexpected byte size - got %d, "+
"want %d", i, j, gotSize, expectedSize)
}
}
// Get the key that doesn't exist in the treap snapshot and
// ensure it is nil.
if gotVal := treapSnap.Get(key); gotVal != nil {
t.Fatalf("Get #%d: unexpected value - got %x, want nil",
i, gotVal)
}
// Ensure the expected size is reported.
if gotSize := treapSnap.Size(); gotSize != expectedSize {
t.Fatalf("Size #%d: unexpected byte size - got %d, "+
"want %d", i, gotSize, expectedSize)
}
expectedSize += (nodeFieldsSize + 8)
expectedSize += (nodeFieldsSize + 8) * uint64(keyCount)
}
// Delete the keys one-by-one while checking several of the treap

View file

@ -496,10 +496,14 @@ testLoop:
for i, test := range tests {
// Insert a bunch of keys.
testTreap := NewImmutable()
keys := make([][]byte, 0, test.numKeys)
kvPairs := make([]KVPair, 0, test.numKeys)
for i := 0; i < test.numKeys; i += test.step {
key := serializeUint32(uint32(i))
testTreap = testTreap.Put(key, key)
keys = append(keys, key)
kvPairs = append(kvPairs, KVPair{key, key})
}
testTreap = testTreap.Put(kvPairs...)
// Create new iterator limited by the test params.
iter := testTreap.Iterator(test.startKey, test.limitKey)