Skip to content

Commit 8693da9

Browse files
WIZARD-CXYxiang90
authored andcommitted
use segregated hashmap to boost the freelist allocate and release performance (#141)
1 parent 26245f2 commit 8693da9

File tree

7 files changed

+450
-48
lines changed

7 files changed

+450
-48
lines changed

Makefile

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ GOLDFLAGS="-X main.branch $(BRANCH) -X main.commit $(COMMIT)"
55
default: build
66

77
race:
8-
@go test -v -race -test.run="TestSimulate_(100op|1000op)"
8+
@TEST_FREELIST_TYPE=hashmap go test -v -race -test.run="TestSimulate_(100op|1000op)"
9+
@echo "array freelist test"
10+
@TEST_FREELIST_TYPE=array go test -v -race -test.run="TestSimulate_(100op|1000op)"
911

1012
fmt:
1113
!(gofmt -l -s -d $(shell find . -name \*.go) | grep '[a-z]')
@@ -23,8 +25,14 @@ errcheck:
2325
@errcheck -ignorepkg=bytes -ignore=os:Remove go.etcd.io/bbolt
2426

2527
test:
26-
go test -timeout 20m -v -coverprofile cover.out -covermode atomic
28+
TEST_FREELIST_TYPE=hashmap go test -timeout 20m -v -coverprofile cover.out -covermode atomic
2729
# Note: gets "program not an importable package" in out of path builds
28-
go test -v ./cmd/bbolt
30+
TEST_FREELIST_TYPE=hashmap go test -v ./cmd/bbolt
31+
32+
@echo "array freelist test"
33+
34+
@TEST_FREELIST_TYPE=array go test -timeout 20m -v -coverprofile cover.out -covermode atomic
35+
# Note: gets "program not an importable package" in out of path builds
36+
@TEST_FREELIST_TYPE=array go test -v ./cmd/bbolt
2937

3038
.PHONY: race fmt errcheck test gosimple unused

allocate_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import (
55
)
66

77
func TestTx_allocatePageStats(t *testing.T) {
8-
f := newFreelist()
8+
f := newTestFreelist()
99
ids := []pgid{2, 3}
1010
f.readIDs(ids)
1111

db.go

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,16 @@ var defaultPageSize = os.Getpagesize()
4343
// The time elapsed between consecutive file locking attempts.
4444
const flockRetryTimeout = 50 * time.Millisecond
4545

46+
// FreelistType is the type of the freelist backend
47+
type FreelistType string
48+
49+
const (
50+
// FreelistArrayType indicates backend freelist type is array
51+
FreelistArrayType = FreelistType("array")
52+
// FreelistMapType indicates backend freelist type is hashmap
53+
FreelistMapType = FreelistType("hashmap")
54+
)
55+
4656
// DB represents a collection of buckets persisted to a file on disk.
4757
// All data access is performed through transactions which can be obtained through the DB.
4858
// All the functions on DB will return a ErrDatabaseNotOpen if accessed before Open() is called.
@@ -70,6 +80,13 @@ type DB struct {
7080
// re-sync during recovery.
7181
NoFreelistSync bool
7282

83+
// FreelistType sets the backend freelist type. There are two options. Array which is simple but endures
84+
// dramatic performance degradation if database is large and framentation in freelist is common.
85+
// The alternative one is using hashmap, it is faster in almost all circumstances
86+
// but it doesn't guarantee that it offers the smallest page id available. In normal case it is safe.
87+
// The default type is array
88+
FreelistType FreelistType
89+
7390
// When true, skips the truncate call when growing the database.
7491
// Setting this to true is only safe on non-ext3/ext4 systems.
7592
// Skipping truncation avoids preallocation of hard drive space and
@@ -169,6 +186,7 @@ func Open(path string, mode os.FileMode, options *Options) (*DB, error) {
169186
db.NoGrowSync = options.NoGrowSync
170187
db.MmapFlags = options.MmapFlags
171188
db.NoFreelistSync = options.NoFreelistSync
189+
db.FreelistType = options.FreelistType
172190

173191
// Set default values for later DB operations.
174192
db.MaxBatchSize = DefaultMaxBatchSize
@@ -283,15 +301,15 @@ func Open(path string, mode os.FileMode, options *Options) (*DB, error) {
283301
// concurrent accesses being made to the freelist.
284302
func (db *DB) loadFreelist() {
285303
db.freelistLoad.Do(func() {
286-
db.freelist = newFreelist()
304+
db.freelist = newFreelist(db.FreelistType)
287305
if !db.hasSyncedFreelist() {
288306
// Reconstruct free list by scanning the DB.
289307
db.freelist.readIDs(db.freepages())
290308
} else {
291309
// Read free list from freelist page.
292310
db.freelist.read(db.page(db.meta().freelist))
293311
}
294-
db.stats.FreePageN = len(db.freelist.getFreePageIDs())
312+
db.stats.FreePageN = db.freelist.free_count()
295313
})
296314
}
297315

@@ -1005,6 +1023,13 @@ type Options struct {
10051023
// under normal operation, but requires a full database re-sync during recovery.
10061024
NoFreelistSync bool
10071025

1026+
// FreelistType sets the backend freelist type. There are two options. Array which is simple but endures
1027+
// dramatic performance degradation if database is large and framentation in freelist is common.
1028+
// The alternative one is using hashmap, it is faster in almost all circumstances
1029+
// but it doesn't guarantee that it offers the smallest page id available. In normal case it is safe.
1030+
// The default type is array
1031+
FreelistType FreelistType
1032+
10081033
// Open database in read-only mode. Uses flock(..., LOCK_SH |LOCK_NB) to
10091034
// grab a shared lock (UNIX).
10101035
ReadOnly bool
@@ -1034,8 +1059,9 @@ type Options struct {
10341059
// DefaultOptions represent the options used if nil options are passed into Open().
10351060
// No timeout is used which will cause Bolt to wait indefinitely for a lock.
10361061
var DefaultOptions = &Options{
1037-
Timeout: 0,
1038-
NoGrowSync: false,
1062+
Timeout: 0,
1063+
NoGrowSync: false,
1064+
FreelistType: FreelistArrayType,
10391065
}
10401066

10411067
// Stats represents statistics about the database.

db_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1605,6 +1605,16 @@ func MustOpenDB() *DB {
16051605
// MustOpenDBWithOption returns a new, open DB at a temporary location with given options.
16061606
func MustOpenWithOption(o *bolt.Options) *DB {
16071607
f := tempfile()
1608+
if o == nil {
1609+
o = bolt.DefaultOptions
1610+
}
1611+
1612+
freelistType := bolt.FreelistArrayType
1613+
if env := os.Getenv(bolt.TestFreelistType); env == string(bolt.FreelistMapType) {
1614+
freelistType = bolt.FreelistMapType
1615+
}
1616+
o.FreelistType = freelistType
1617+
16081618
db, err := bolt.Open(f, 0666, o)
16091619
if err != nil {
16101620
panic(err)

freelist.go

Lines changed: 66 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,54 @@ type txPending struct {
1414
lastReleaseBegin txid // beginning txid of last matching releaseRange
1515
}
1616

17+
// pidSet holds the set of starting pgids which have the same span size
18+
type pidSet map[pgid]struct{}
19+
1720
// freelist represents a list of all pages that are available for allocation.
1821
// It also tracks pages that have been freed but are still in use by open transactions.
1922
type freelist struct {
20-
ids []pgid // all free and available free page ids.
21-
allocs map[pgid]txid // mapping of txid that allocated a pgid.
22-
pending map[txid]*txPending // mapping of soon-to-be free page ids by tx.
23-
cache map[pgid]bool // fast lookup of all free and pending page ids.
23+
freelistType FreelistType // freelist type
24+
ids []pgid // all free and available free page ids.
25+
allocs map[pgid]txid // mapping of txid that allocated a pgid.
26+
pending map[txid]*txPending // mapping of soon-to-be free page ids by tx.
27+
cache map[pgid]bool // fast lookup of all free and pending page ids.
28+
freemaps map[uint64]pidSet // key is the size of continuous pages(span), value is a set which contains the starting pgids of same size
29+
forwardMap map[pgid]uint64 // key is start pgid, value is its span size
30+
backwardMap map[pgid]uint64 // key is end pgid, value is its span size
31+
allocate func(txid txid, n int) pgid // the freelist allocate func
32+
free_count func() int // the function which gives you free page number
33+
mergeSpans func(ids pgids) // the mergeSpan func
34+
getFreePageIDs func() []pgid // get free pgids func
35+
readIDs func(pgids []pgid) // readIDs func reads list of pages and init the freelist
2436
}
2537

2638
// newFreelist returns an empty, initialized freelist.
27-
func newFreelist() *freelist {
28-
return &freelist{
29-
allocs: make(map[pgid]txid),
30-
pending: make(map[txid]*txPending),
31-
cache: make(map[pgid]bool),
39+
func newFreelist(freelistType FreelistType) *freelist {
40+
f := &freelist{
41+
freelistType: freelistType,
42+
allocs: make(map[pgid]txid),
43+
pending: make(map[txid]*txPending),
44+
cache: make(map[pgid]bool),
45+
freemaps: make(map[uint64]pidSet),
46+
forwardMap: make(map[pgid]uint64),
47+
backwardMap: make(map[pgid]uint64),
48+
}
49+
50+
if freelistType == FreelistMapType {
51+
f.allocate = f.hashmapAllocate
52+
f.free_count = f.hashmapFreeCount
53+
f.mergeSpans = f.hashmapMergeSpans
54+
f.getFreePageIDs = f.hashmapGetFreePageIDs
55+
f.readIDs = f.hashmapReadIDs
56+
} else {
57+
f.allocate = f.arrayAllocate
58+
f.free_count = f.arrayFreeCount
59+
f.mergeSpans = f.arrayMergeSpans
60+
f.getFreePageIDs = f.arrayGetFreePageIDs
61+
f.readIDs = f.arrayReadIDs
3262
}
63+
64+
return f
3365
}
3466

3567
// size returns the size of the page after serialization.
@@ -47,8 +79,8 @@ func (f *freelist) count() int {
4779
return f.free_count() + f.pending_count()
4880
}
4981

50-
// free_count returns count of free pages
51-
func (f *freelist) free_count() int {
82+
// arrayFreeCount returns count of free pages(array version)
83+
func (f *freelist) arrayFreeCount() int {
5284
return len(f.ids)
5385
}
5486

@@ -72,9 +104,9 @@ func (f *freelist) copyall(dst []pgid) {
72104
mergepgids(dst, f.getFreePageIDs(), m)
73105
}
74106

75-
// allocate returns the starting page id of a contiguous list of pages of a given size.
107+
// arrayAllocate returns the starting page id of a contiguous list of pages of a given size.
76108
// If a contiguous block cannot be found then 0 is returned.
77-
func (f *freelist) allocate(txid txid, n int) pgid {
109+
func (f *freelist) arrayAllocate(txid txid, n int) pgid {
78110
if len(f.ids) == 0 {
79111
return 0
80112
}
@@ -160,8 +192,7 @@ func (f *freelist) release(txid txid) {
160192
delete(f.pending, tid)
161193
}
162194
}
163-
sort.Sort(m)
164-
f.ids = pgids(f.ids).merge(m)
195+
f.mergeSpans(m)
165196
}
166197

167198
// releaseRange moves pending pages allocated within an extent [begin,end] to the free list.
@@ -194,8 +225,7 @@ func (f *freelist) releaseRange(begin, end txid) {
194225
delete(f.pending, tid)
195226
}
196227
}
197-
sort.Sort(m)
198-
f.ids = pgids(f.ids).merge(m)
228+
f.mergeSpans(m)
199229
}
200230

201231
// rollback removes the pages from a given pending tx.
@@ -222,8 +252,7 @@ func (f *freelist) rollback(txid txid) {
222252
}
223253
// Remove pages from pending list and mark as free if allocated by txid.
224254
delete(f.pending, txid)
225-
sort.Sort(m)
226-
f.ids = pgids(f.ids).merge(m)
255+
f.mergeSpans(m)
227256
}
228257

229258
// freed returns whether a given page is in the free list.
@@ -249,24 +278,24 @@ func (f *freelist) read(p *page) {
249278
f.ids = nil
250279
} else {
251280
ids := ((*[maxAllocSize]pgid)(unsafe.Pointer(&p.ptr)))[idx : idx+count]
252-
f.ids = make([]pgid, len(ids))
253-
copy(f.ids, ids)
254281

282+
// copy the ids, so we don't modify on the freelist page directly
283+
idsCopy := make([]pgid, count)
284+
copy(idsCopy, ids)
255285
// Make sure they're sorted.
256-
sort.Sort(pgids(f.ids))
257-
}
286+
sort.Sort(pgids(idsCopy))
258287

259-
// Rebuild the page cache.
260-
f.reindex()
288+
f.readIDs(idsCopy)
289+
}
261290
}
262291

263-
// readIDs initializes the freelist from a given list of ids.
264-
func (f *freelist) readIDs(ids []pgid) {
292+
// arrayReadIDs initializes the freelist from a given list of ids.
293+
func (f *freelist) arrayReadIDs(ids []pgid) {
265294
f.ids = ids
266295
f.reindex()
267296
}
268297

269-
func (f *freelist) getFreePageIDs() []pgid {
298+
func (f *freelist) arrayGetFreePageIDs() []pgid {
270299
return f.ids
271300
}
272301

@@ -322,8 +351,9 @@ func (f *freelist) reload(p *page) {
322351

323352
// reindex rebuilds the free cache based on available and pending free lists.
324353
func (f *freelist) reindex() {
325-
f.cache = make(map[pgid]bool, len(f.getFreePageIDs()))
326-
for _, id := range f.getFreePageIDs() {
354+
ids := f.getFreePageIDs()
355+
f.cache = make(map[pgid]bool, len(ids))
356+
for _, id := range ids {
327357
f.cache[id] = true
328358
}
329359
for _, txp := range f.pending {
@@ -332,3 +362,9 @@ func (f *freelist) reindex() {
332362
}
333363
}
334364
}
365+
366+
// arrayMergeSpans try to merge list of pages(represented by pgids) with existing spans but using array
367+
func (f *freelist) arrayMergeSpans(ids pgids) {
368+
sort.Sort(ids)
369+
f.ids = pgids(f.ids).merge(ids)
370+
}

0 commit comments

Comments
 (0)