Skip to content

Commit

Permalink
reduce write operation memory allocations by only allocating ListElem…
Browse files Browse the repository at this point in the history
…ent when needed
  • Loading branch information
cornelk committed Aug 29, 2022
1 parent e90d44f commit 517efe3
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 37 deletions.
76 changes: 57 additions & 19 deletions hashmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ func (m *HashMap[Key, Value]) GetOrInsert(key Key, value Value) (Value, bool) {
newElement.value.Store(&value)
}

if m.insertElement(newElement, false) {
if m.insertElement(newElement, hash, key, value) {
return value, false
}
}
Expand Down Expand Up @@ -139,25 +139,64 @@ func (m *HashMap[Key, Value]) Del(key Key) bool {
// Returns true if the item was inserted or false if it existed.
func (m *HashMap[Key, Value]) Insert(key Key, value Value) bool {
hash := m.hasher(key)
element := &ListElement[Key, Value]{
key: key,
keyHash: hash,
var (
existed, inserted bool
element *ListElement[Key, Value]
)

for {
store := m.store.Load()
searchStart := store.item(hash)

if !inserted { // if retrying after insert during grow, do not add to list again
element, existed, inserted = m.linkedList.Add(searchStart, hash, key, value)
if existed {
return false
}
if !inserted {
continue // a concurrent add did interfere, try again
}
}

count := store.addItem(element)
currentStore := m.store.Load()
if store != currentStore { // retry insert in case of insert during grow
continue
}

if m.isResizeNeeded(store, count) && m.resizing.CompareAndSwap(0, 1) {
go m.grow(0, true)
}
return true
}
element.value.Store(&value)
return m.insertElement(element, false)
}

// Set sets the value under the specified key to the map. An existing item for this key will be overwritten.
// If a resizing operation is happening concurrently while calling Set, the item might show up in the map
// after the resize operation is finished.
func (m *HashMap[Key, Value]) Set(key Key, value Value) {
hash := m.hasher(key)
element := &ListElement[Key, Value]{
key: key,
keyHash: hash,

for {
store := m.store.Load()
searchStart := store.item(hash)

element, added := m.linkedList.AddOrUpdate(searchStart, hash, key, value)
if !added {
continue // a concurrent add did interfere, try again
}

count := store.addItem(element)
currentStore := m.store.Load()
if store != currentStore { // retry insert in case of insert during grow
continue
}

if m.isResizeNeeded(store, count) && m.resizing.CompareAndSwap(0, 1) {
go m.grow(0, true)
}
return
}
element.value.Store(&value)
m.insertElement(element, true)
}

// Grow resizes the hashmap to a new size, the size gets rounded up to next power of 2.
Expand Down Expand Up @@ -265,23 +304,22 @@ func (m *HashMap[Key, Value]) searchItem(item *ListElement[Key, Value], key Key,
}
*/

func (m *HashMap[Key, Value]) insertElement(element *ListElement[Key, Value], update bool) bool {
func (m *HashMap[Key, Value]) insertElement(element *ListElement[Key, Value], hash uintptr, key Key, value Value) bool {
var existed, inserted bool

for {
store := m.store.Load()
searchStart := store.item(element.keyHash)

if update {
inserted = m.linkedList.AddOrUpdate(element, searchStart)
} else if !inserted { // if retrying after insert during grow, do not add to list again
existed, inserted = m.linkedList.Add(element, searchStart)
if !inserted { // if retrying after insert during grow, do not add to list again
_, existed, inserted = m.linkedList.Add(searchStart, hash, key, value)
if existed {
return false
}
}
if !inserted {
continue // a concurrent add did interfere, try again

if !inserted {
continue // a concurrent add did interfere, try again
}
}

count := store.addItem(element)
Expand Down
10 changes: 5 additions & 5 deletions hashmap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -426,9 +426,9 @@ func TestConcurrentInsertDelete(t *testing.T) {
keyHash: 223,
}
l := NewList[int, int]()
l.Add(el1, nil)
l.Add(el2, nil)
l.Add(el3, nil)
l.Add(nil, el1.keyHash, el1.key, 111)
l.Add(nil, el2.keyHash, el2.key, 222)
l.Add(nil, el3.keyHash, el3.key, 333)
wg := sync.WaitGroup{}
wg.Add(2)

Expand All @@ -443,15 +443,15 @@ func TestConcurrentInsertDelete(t *testing.T) {
rand.Seed(int64(time.Now().Nanosecond()))
time.Sleep(time.Duration(rand.Intn(10)))
for {
if _, inserted := l.Add(newIl, nil); inserted {
if _, _, inserted := l.Add(nil, newIl.keyHash, newIl.key, 223); inserted {
return
}
}
}()
wg.Wait()

assert.Equal(t, 3, l.Len())
_, found, _ := l.search(nil, newIl)
_, found, _ := l.search(nil, newIl.keyHash, newIl.key)
assert.NotNil(t, found)
}
}
36 changes: 23 additions & 13 deletions list.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,24 +29,34 @@ func (l *List[Key, Value]) First() *ListElement[Key, Value] {

// Add adds an item to the list and returns false if an item for the hash existed.
// searchStart = nil will start to search at the head item.
func (l *List[Key, Value]) Add(element, searchStart *ListElement[Key, Value]) (existed bool, inserted bool) {
left, found, right := l.search(searchStart, element)
func (l *List[Key, Value]) Add(searchStart *ListElement[Key, Value], hash uintptr, key Key, value Value) (element *ListElement[Key, Value], existed bool, inserted bool) {
left, found, right := l.search(searchStart, hash, key)
if found != nil { // existing item found
return true, false
return found, true, false
}

return false, l.insertAt(element, left, right)
element = &ListElement[Key, Value]{
key: key,
keyHash: hash,
}
element.value.Store(&value)
return element, false, l.insertAt(element, left, right)
}

// AddOrUpdate adds or updates an item to the list.
func (l *List[Key, Value]) AddOrUpdate(element, searchStart *ListElement[Key, Value]) bool {
left, found, right := l.search(searchStart, element)
func (l *List[Key, Value]) AddOrUpdate(searchStart *ListElement[Key, Value], hash uintptr, key Key, value Value) (*ListElement[Key, Value], bool) {
left, found, right := l.search(searchStart, hash, key)
if found != nil { // existing item found
found.value.Store(element.value.Load()) // update the value
return true
found.value.Store(&value) // update the value
return found, true
}

return l.insertAt(element, left, right)
element := &ListElement[Key, Value]{
key: key,
keyHash: hash,
}
element.value.Store(&value)
return element, l.insertAt(element, left, right)
}

// Delete deletes an element from the list.
Expand All @@ -65,8 +75,8 @@ func (l *List[Key, Value]) Delete(element *ListElement[Key, Value]) {
l.count.Add(^uintptr(0)) // decrease counter
}

func (l *List[Key, Value]) search(searchStart, item *ListElement[Key, Value]) (left, found, right *ListElement[Key, Value]) {
if searchStart != nil && item.keyHash < searchStart.keyHash { // key would remain left from item? {
func (l *List[Key, Value]) search(searchStart *ListElement[Key, Value], hash uintptr, key Key) (left, found, right *ListElement[Key, Value]) {
if searchStart != nil && hash < searchStart.keyHash { // key would remain left from item? {
searchStart = nil // start search at head
}

Expand All @@ -81,11 +91,11 @@ func (l *List[Key, Value]) search(searchStart, item *ListElement[Key, Value]) (l
}

for {
if item.keyHash == found.keyHash && item.key == found.key { // key hash already exists, compare keys
if hash == found.keyHash && key == found.key { // key hash already exists, compare keys
return nil, found, nil
}

if item.keyHash < found.keyHash { // new item needs to be inserted before the found value
if hash < found.keyHash { // new item needs to be inserted before the found value
if l.head == left {
return nil, nil, found
}
Expand Down

0 comments on commit 517efe3

Please sign in to comment.