Skip to content

Commit

Permalink
Add presized constructors for Map and MapOf (#86)
Browse files Browse the repository at this point in the history
Also adds missing b.ResetTimer() to map benchmarks.
  • Loading branch information
puzpuzpuz authored Nov 5, 2022
1 parent a140d88 commit 4830442
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 86 deletions.
1 change: 1 addition & 0 deletions export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const (
EntriesPerMapBucket = entriesPerMapBucket
MapLoadFactor = mapLoadFactor
MinMapTableLen = minMapTableLen
MinMapTableCap = minMapTableCap
MaxMapCounterLen = maxMapCounterLen
)

Expand Down
22 changes: 18 additions & 4 deletions map.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ const (
// minimal table size, i.e. number of buckets; thus, minimal map
// capacity can be calculated as entriesPerMapBucket*minMapTableLen
minMapTableLen = 32
// minimal table capacity
minMapTableCap = minMapTableLen * entriesPerMapBucket
// minimum counter stripes to use
minMapCounterLen = 8
// maximum counter stripes to use; stands for around 4KB of memory
Expand Down Expand Up @@ -120,16 +122,28 @@ type rangeEntry struct {

// NewMap creates a new Map instance.
func NewMap() *Map {
return NewMapPresized(minMapTableCap)
}

// NewMapPresized creates a new Map instance with capacity enough to hold
// sizeHint entries. If sizeHint is zero or negative, the value is ignored.
func NewMapPresized(sizeHint int) *Map {
m := &Map{}
m.resizeCond = *sync.NewCond(&m.resizeMu)
table := newMapTable(minMapTableLen)
var table *mapTable
if sizeHint <= minMapTableCap {
table = newMapTable(minMapTableLen)
} else {
tableLen := nextPowOf2(uint32(sizeHint / entriesPerMapBucket))
table = newMapTable(int(tableLen))
}
atomic.StorePointer(&m.table, unsafe.Pointer(table))
return m
}

func newMapTable(size int) *mapTable {
buckets := make([]bucketPadded, size)
counterLen := size >> 10
func newMapTable(tableLen int) *mapTable {
buckets := make([]bucketPadded, tableLen)
counterLen := tableLen >> 10
if counterLen < minMapCounterLen {
counterLen = minMapCounterLen
} else if counterLen > maxMapCounterLen {
Expand Down
33 changes: 23 additions & 10 deletions map_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ var benchmarkCases = []struct {
name string
readPercentage int
}{
{"reads=100%", 100}, // 100% loads, 0% stores, 0% deletes
{"reads=99%", 99}, // 99% loads, 0.5% stores, 0.5% deletes
{"reads=90%-reads", 90}, // 90% loads, 5% stores, 5% deletes
{"reads=75%-reads", 75}, // 75% loads, 12.5% stores, 12.5% deletes
{"reads=100%", 100}, // 100% loads, 0% stores, 0% deletes
{"reads=99%", 99}, // 99% loads, 0.5% stores, 0.5% deletes
{"reads=90%", 90}, // 90% loads, 5% stores, 5% deletes
{"reads=75%", 75}, // 75% loads, 12.5% stores, 12.5% deletes
}

var benchmarkKeys []string
Expand Down Expand Up @@ -534,6 +534,20 @@ func TestMapClear(t *testing.T) {
}
}

func assertMapCapacity(t *testing.T, m *Map, expectedCap int) {
stats := CollectMapStats(m)
if stats.Capacity != expectedCap {
t.Fatalf("capacity was different from %d: %d", expectedCap, stats.Capacity)
}
}

func TestNewMapPresized(t *testing.T) {
assertMapCapacity(t, NewMap(), MinMapTableCap)
assertMapCapacity(t, NewMapPresized(1000), 1536)
assertMapCapacity(t, NewMapPresized(0), MinMapTableCap)
assertMapCapacity(t, NewMapPresized(-1), MinMapTableCap)
}

func TestMapResize(t *testing.T) {
const numEntries = 100_000
m := NewMap()
Expand Down Expand Up @@ -1002,11 +1016,6 @@ func testMapTopHashMutex_StoreAfterErase(t *testing.T, topHashes *uint64) {
}
}

type SyncMap interface {
Load(key string) (value interface{}, ok bool)
Store(key string, value interface{})
}

func BenchmarkMap_NoWarmUp(b *testing.B) {
for _, bc := range benchmarkCases {
if bc.readPercentage == 100 {
Expand Down Expand Up @@ -1048,10 +1057,11 @@ func BenchmarkMapStandard_NoWarmUp(b *testing.B) {
func BenchmarkMap_WarmUp(b *testing.B) {
for _, bc := range benchmarkCases {
b.Run(bc.name, func(b *testing.B) {
m := NewMap()
m := NewMapPresized(benchmarkNumEntries)
for i := 0; i < benchmarkNumEntries; i++ {
m.Store(benchmarkKeyPrefix+strconv.Itoa(i), i)
}
b.ResetTimer()
benchmarkMap(b, func(k string) (interface{}, bool) {
return m.Load(k)
}, func(k string, v interface{}) {
Expand All @@ -1072,6 +1082,7 @@ func BenchmarkMapStandard_WarmUp(b *testing.B) {
for i := 0; i < benchmarkNumEntries; i++ {
m.Store(benchmarkKeyPrefix+strconv.Itoa(i), i)
}
b.ResetTimer()
benchmarkMap(b, func(k string) (interface{}, bool) {
return m.Load(k)
}, func(k string, v interface{}) {
Expand Down Expand Up @@ -1113,6 +1124,7 @@ func BenchmarkMapRange(b *testing.B) {
for i := 0; i < benchmarkNumEntries; i++ {
m.Store(benchmarkKeys[i], i)
}
b.ResetTimer()
runParallel(b, func(pb *testing.PB) {
foo := 0
for pb.Next() {
Expand All @@ -1133,6 +1145,7 @@ func BenchmarkMapRangeStandard(b *testing.B) {
for i := 0; i < benchmarkNumEntries; i++ {
m.Store(benchmarkKeys[i], i)
}
b.ResetTimer()
runParallel(b, func(pb *testing.PB) {
foo := 0
for pb.Next() {
Expand Down
46 changes: 39 additions & 7 deletions mapof.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,16 @@ type entryOf[K comparable, V any] struct {
value V
}

// NewMapOf creates a new MapOf instance with string keys
// NewMapOf creates a new MapOf instance with string keys.
func NewMapOf[V any]() *MapOf[string, V] {
return NewTypedMapOf[string, V](hashString)
return NewTypedMapOfPresized[string, V](hashString, minMapTableCap)
}

// NewMapOfPresized creates a new MapOf instance with string keys and capacity
// enough to hold sizeHint entries. If sizeHint is zero or negative, the value
// is ignored.
func NewMapOfPresized[V any](sizeHint int) *MapOf[string, V] {
return NewTypedMapOfPresized[string, V](hashString, sizeHint)
}

// IntegerConstraint represents any integer type.
Expand All @@ -82,25 +89,50 @@ type IntegerConstraint interface {

// NewIntegerMapOf creates a new MapOf instance with integer typed keys.
func NewIntegerMapOf[K IntegerConstraint, V any]() *MapOf[K, V] {
return NewTypedMapOf[K, V](hashUint64[K])
return NewTypedMapOfPresized[K, V](hashUint64[K], minMapTableCap)
}

// NewIntegerMapOfPresized creates a new MapOf instance with integer typed keys
// and capacity enough to hold sizeHint entries. If sizeHint is zero or
// negative, the value is ignored.
func NewIntegerMapOfPresized[K IntegerConstraint, V any](sizeHint int) *MapOf[K, V] {
return NewTypedMapOfPresized[K, V](hashUint64[K], sizeHint)
}

// NewTypedMapOf creates a new MapOf instance with arbitrarily typed keys.
//
// Keys are hashed to uint64 using the hasher function. It is strongly
// recommended to use the hash/maphash package to implement hasher. See the
// example for how to do that.
func NewTypedMapOf[K comparable, V any](hasher func(maphash.Seed, K) uint64) *MapOf[K, V] {
return NewTypedMapOfPresized[K, V](hasher, minMapTableCap)
}

// NewTypedMapOfPresized creates a new MapOf instance with arbitrarily typed
// keys and capacity enough to hold sizeHint entries. If sizeHint is zero or
// negative, the value is ignored.
//
// Keys are hashed to uint64 using the hasher function. It is strongly
// recommended to use the hash/maphash package to implement hasher. See the
// example for how to do that.
func NewTypedMapOfPresized[K comparable, V any](hasher func(maphash.Seed, K) uint64, sizeHint int) *MapOf[K, V] {
m := &MapOf[K, V]{}
m.resizeCond = *sync.NewCond(&m.resizeMu)
m.hasher = hasher
table := newMapOfTable[K, V](minMapTableLen)
var table *mapOfTable[K, V]
if sizeHint <= minMapTableCap {
table = newMapOfTable[K, V](minMapTableLen)
} else {
tableLen := nextPowOf2(uint32(sizeHint / entriesPerMapBucket))
table = newMapOfTable[K, V](int(tableLen))
}
atomic.StorePointer(&m.table, unsafe.Pointer(table))
return m
}

func newMapOfTable[K comparable, V any](size int) *mapOfTable[K, V] {
buckets := make([]bucketOfPadded, size)
counterLen := size >> 10
func newMapOfTable[K comparable, V any](tableLen int) *mapOfTable[K, V] {
buckets := make([]bucketOfPadded, tableLen)
counterLen := tableLen >> 10
if counterLen < minMapCounterLen {
counterLen = minMapCounterLen
} else if counterLen > maxMapCounterLen {
Expand Down
Loading

0 comments on commit 4830442

Please sign in to comment.