rosedblabs / rosedb

Lightweight, fast and reliable key/value storage engine based on Bitcask.
https://rosedblabs.github.io
Apache License 2.0
4.48k stars 620 forks source link

perf: improve batch performance #303

Closed Sora233 closed 3 months ago

Sora233 commented 4 months ago

For now, Batch.pendingWrites prevents for large batch put. Actually, Put in batch is O(N^2). (Maybe not only Put, I havn't check other operations yet.) https://github.com/rosedblabs/rosedb/blob/a776163adb0c23b8b2cf1982464a659602fbf435/batch.go#L121-L126 try example:

package main

import (
    "fmt"
    "github.com/rosedblabs/rosedb/v2"
    "strconv"
    "time"
)

func main() {
    opts := rosedb.DefaultOptions
    db, err := rosedb.Open(opts)
    if err != nil {
        return
    }
    if err != nil {
        panic(err)
    }
    defer os.RemoveAll(opts.DirPath)
    defer db.Close()

    batch := db.NewBatch(rosedb.DefaultBatchOptions)

    start := time.Now()

    for i := 0; i < 1e5; i++ {
        batch.Put([]byte(strconv.Itoa(i)), []byte("xxx"))
    }
    err = batch.Commit()
    if err != nil {
        panic(err)
    }
    fmt.Println(time.Now().Sub(start))
}

It shows 22sec+ on my pc.

Solution:

I add a pendingWritesMap to help finding out the key in pendingWrites. I also add a benchmark for batch.

Before:

goos: linux
goarch: amd64
pkg: github.com/rosedblabs/rosedb/v2/benchmark
cpu: AMD Ryzen 7 5800X3D 8-Core Processor
BenchmarkBatchPutGet
BenchmarkBatchPutGet/batchPut
BenchmarkBatchPutGet/batchPut-16                   24601             86200 ns/o
  11499 B/op          12 allocs/op
BenchmarkBatchPutGet/batchGet
BenchmarkBatchPutGet/batchGet-16                 1789173               658.6 ns/
op           135 B/op          4 allocs/op

After:

goos: linux
goarch: amd64
pkg: github.com/rosedblabs/rosedb/v2/benchmark
cpu: AMD Ryzen 7 5800X3D 8-Core Processor
BenchmarkBatchPutGet
BenchmarkBatchPutGet/batchPut
BenchmarkBatchPutGet/batchPut-16                   64142             19151 ns/o
  11317 B/op          13 allocs/op
BenchmarkBatchPutGet/batchGet
BenchmarkBatchPutGet/batchGet-16                 1770322               677.3 ns/
op           135 B/op          4 allocs/op
roseduan commented 4 months ago

Thanks, but I do not know whether it is necessary, even though it has a little performance improvement, it also takes more memory to hold the pending map.

Sora233 commented 4 months ago

If increase 1e5 to 1e7 in example, the program likely runs for more than one hour. Maybe a better trade-off is only using the map for more than 100(or any reasonable number) pendings.

roseduan commented 4 months ago

If increase 1e5 to 1e7 in example, the program likely runs for more than one hour. Maybe a better trade-off is only using the map for more than 100(or any reasonable number) pendings.

I have rethought the issue, we can hold the map to improve performance, but we do not need to record the key in the map. Because if the key size is large, it may have more memory cost.

So we can record the key hash in the map, like map[hash(key)][index], the hash function is not necessary to be persistent, because we only need it when the batch exists, if the batch commit or rollback, it will no longer use. So we can use a memory hash algorithm for this, you can see the usage in badger: https://github.com/dgraph-io/badger/blob/main/txn.go#L396

Sora233 commented 3 months ago

So we can use a memory hash algorithm for this, you can see the usage in badger: https://github.com/dgraph-io/badger/blob/main/txn.go#L396

I have implemented this hash mechanism by looking into the code you provided.