Skip to content

Commit

Permalink
Support nil values in Map (#6)
Browse files Browse the repository at this point in the history
  • Loading branch information
puzpuzpuz authored Aug 12, 2021
1 parent 97a5de7 commit da1aa49
Show file tree
Hide file tree
Showing 3 changed files with 47 additions and 31 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ v, ok := m.Load("foo")

CLHT is built around idea to organize the hash table in cache-line-sized buckets, so that on all modern CPUs update operations complete with at most one cache-line transfer. Also, Get operations involve no write to memory, as well as no mutexes or any other sort of locks. Due to this design, in all considered scenarios Map outperforms sync.Map.

One important difference with sync.Map is that only string keys are supported. That's because Golang standard library does not expose the built-in hash functions for `interface{}` values. Another difference with sync.Map is that nil values are not supported. Use Delete operation or a special "nil" value to overcome this restriction.
One important difference with sync.Map is that only string keys are supported. That's because Golang standard library does not expose the built-in hash functions for `interface{}` values.

## MPMCQueue

Expand Down
46 changes: 28 additions & 18 deletions map.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,7 @@ const (
//
// One important difference with sync.Map is that only string keys
// are supported. That's because Golang standard library does not
// expose the built-in hash functions for interface{} values. Another
// difference with sync.Map is that nil values are not supported. Use
// Delete operation or a special "nil" value to overcome this
// restriction.
// expose the built-in hash functions for interface{} values.
//
// Also note that, unlike in sync.Map, the underlying hash table used
// by Map never shrinks and only grows on demand. However, this
Expand All @@ -65,6 +62,11 @@ type rangeEntry struct {
value unsafe.Pointer
}

// special type to mark nil values
type nilValue struct{}

var nilVal = new(nilValue)

// NewMap creates a new Map instance.
func NewMap() *Map {
m := &Map{}
Expand All @@ -89,10 +91,10 @@ func (m *Map) Load(key string) (value interface{}, ok bool) {
vp := atomic.LoadPointer(&b.values[i])
kp := atomic.LoadPointer(&b.keys[i])
if kp != nil && vp != nil {
if key == *(*string)(kp) {
if key == derefKey(kp) {
if uintptr(vp) == uintptr(atomic.LoadPointer(&b.values[i])) {
// Atomic snapshot succeeded.
return *(*interface{})(vp), true
return derefValue(vp), true
}
// Concurrent update/remove of the key case. Go for another spin.
continue
Expand All @@ -112,24 +114,20 @@ func (m *Map) Load(key string) (value interface{}, ok bool) {
}

// Store sets the value for a key.
//
// A panic is raised if a nil value is provided.
func (m *Map) Store(key string, value interface{}) {
m.doStore(key, value, false)
}

// LoadOrStore returns the existing value for the key if present.
// Otherwise, it stores and returns the given value.
// The loaded result is true if the value was loaded, false if stored.
//
// A panic is raised if a nil value is provided.
func (m *Map) LoadOrStore(key string, value interface{}) (actual interface{}, loaded bool) {
return m.doStore(key, value, true)
}

func (m *Map) doStore(key string, value interface{}, loadIfExists bool) (actual interface{}, loaded bool) {
if value == nil {
panic("nil values are not supported")
value = nilVal
}
// Read-only path.
if loadIfExists {
Expand Down Expand Up @@ -162,10 +160,10 @@ func (m *Map) doStore(key string, value interface{}, loadIfExists bool) (actual
for {
for i := 0; i < entriesPerMapBucket; i++ {
if b.keys[i] != nil {
k := *(*string)(b.keys[i])
k := derefKey(b.keys[i])
if k == key {
if loadIfExists {
return *(*interface{})(b.values[i]), true
return derefValue(b.values[i]), true
}
// In-place update case. Luckily we get a copy of the value
// interface{} on each call, thus the live value pointers are
Expand Down Expand Up @@ -250,7 +248,7 @@ func copyBucket(b *bucket, table []bucket) {
for {
for i := 0; i < entriesPerMapBucket; i++ {
if b.keys[i] != nil {
k := *(*string)(b.keys[i])
k := derefKey(b.keys[i])
hash := fnv32(k)
destb := &table[uint32(len(table)-1)&hash]
appendToBucket(destb, b.keys[i], b.values[i])
Expand Down Expand Up @@ -310,15 +308,15 @@ func (m *Map) LoadAndDelete(key string) (value interface{}, loaded bool) {
for i := 0; i < entriesPerMapBucket; i++ {
kp := b.keys[i]
if kp != nil {
k := *(*string)(kp)
k := derefKey(kp)
if k == key {
vp := b.values[i]
// Deletion case. First we update the value, then the key.
// This is important for atomic snapshot states.
atomic.StorePointer(&b.values[i], nil)
atomic.StorePointer(&b.keys[i], nil)
rootb.mu.Unlock()
return *(*interface{})(vp), true
return derefValue(vp), true
}
}
}
Expand Down Expand Up @@ -358,8 +356,8 @@ func (m *Map) Range(f func(key string, value interface{}) bool) {
if bentries[j].key == nil {
break
}
k := *(*string)(bentries[j].key)
v := *(*interface{})(bentries[j].value)
k := derefKey(bentries[j].key)
v := derefValue(bentries[j].value)
if !f(k, v) {
return
}
Expand Down Expand Up @@ -394,3 +392,15 @@ func copyRangeEntries(bentries *[]rangeEntry, b *bucket) {
b = (*bucket)(b.next)
}
}

func derefKey(keyPtr unsafe.Pointer) string {
return *(*string)(keyPtr)
}

func derefValue(valuePtr unsafe.Pointer) interface{} {
value := *(*interface{})(valuePtr)
if _, ok := value.(*nilValue); ok {
return nil
}
return value
}
30 changes: 18 additions & 12 deletions map_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ var benchmarkCases = []struct {
{"0%-reads", 0}, // 0% loads, 50% stores, 50% deletes
}

func TestMapBucketStructSize(t *testing.T) {
func TestMap_BucketStructSize(t *testing.T) {
if bits.UintSize != 64 {
return // skip for 32-bit builds
}
Expand All @@ -41,7 +41,7 @@ func TestMapBucketStructSize(t *testing.T) {
}
}

func TestMapMissingEntry(t *testing.T) {
func TestMap_MissingEntry(t *testing.T) {
m := NewMap()
v, ok := m.Load("foo")
if ok {
Expand All @@ -55,22 +55,28 @@ func TestMapMissingEntry(t *testing.T) {
}
}

func TestMapStoreNilValue(t *testing.T) {
func TestMapStore_NilValue(t *testing.T) {
m := NewMap()
defer func() {
recover()
}()
m.Store("foo", nil)
t.Error("no panic was raised")
v, ok := m.Load("foo")
if !ok {
t.Error("nil value was expected")
}
if v != nil {
t.Errorf("value was not nil: %v", v)
}
}

func TestMapLoadOrStoreNilValue(t *testing.T) {
func TestMapLoadOrStore_NilValue(t *testing.T) {
m := NewMap()
defer func() {
recover()
}()
m.LoadOrStore("foo", nil)
t.Error("no panic was raised")
v, loaded := m.LoadOrStore("foo", nil)
if !loaded {
t.Error("nil value was expected")
}
if v != nil {
t.Errorf("value was not nil: %v", v)
}
}

func TestMapRange(t *testing.T) {
Expand Down

0 comments on commit da1aa49

Please sign in to comment.