Skip to content

Commit

Permalink
First version
Browse files Browse the repository at this point in the history
  • Loading branch information
cornelk committed Aug 2, 2016
1 parent 82b10d3 commit f64c8b0
Show file tree
Hide file tree
Showing 3 changed files with 270 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
@@ -0,0 +1,4 @@
.DS_Store
*.iml
.idea
*.exe
160 changes: 160 additions & 0 deletions hashmap.go
@@ -0,0 +1,160 @@
package hashmap

import (
"reflect"
"sync"
"sync/atomic"
"unsafe"
)

const intSizeBytes = 4 << (^uint(0) >> 63)

type (
hashMapEntry struct {
key1 uint64
key2 uint64
value interface{}
}

hashMapData struct {
andMask uint64
data unsafe.Pointer
size uint64
count uint64
slice []*hashMapEntry
}

// HashMap implements a read optimized hash map
HashMap struct {
mapData unsafe.Pointer
sync.Mutex
}
)

func New() *HashMap {
return NewSize(8)
}

func NewSize(size uint64) *HashMap {
hashmap := &HashMap{}
hashmap.Resize(size)
return hashmap
}

// Count returns the number of elements within the map.
func (m *HashMap) Count() uint64 {
mapData := (*hashMapData)(atomic.LoadPointer(&m.mapData))
return atomic.LoadUint64(&mapData.count)
}

// Retrieves an element from map under given key.
func (m *HashMap) Get(key1 uint64, key2 uint64) (interface{}, bool) {
mapData := (*hashMapData)(atomic.LoadPointer(&m.mapData))
index := key1 & mapData.andMask
sliceDataIndexPointer := (*unsafe.Pointer)(unsafe.Pointer(uintptr(mapData.data) + uintptr(index*intSizeBytes)))
entry := (*hashMapEntry)(atomic.LoadPointer(sliceDataIndexPointer))

if entry == nil || key1 != entry.key1 || key2 != entry.key2 {
return nil, false
}

return entry.value, true
}

// Sets the given value under the specified key.
func (m *HashMap) Set(key1 uint64, key2 uint64, value interface{}) {
m.Lock()
defer m.Unlock()

_, exists := m.Get(key1, key2)
if exists {
return
}

mapData := (*hashMapData)(atomic.LoadPointer(&m.mapData))
index := key1 & mapData.andMask

for {
sliceDataIndexPointer := (*unsafe.Pointer)(unsafe.Pointer(uintptr(mapData.data) + uintptr(index*intSizeBytes)))
entry := (*hashMapEntry)(atomic.LoadPointer(sliceDataIndexPointer))
if entry == nil || entry.key1 == key1 { // no hash collision?
break
}
m.Resize(mapData.size + 1)

mapData = (*hashMapData)(atomic.LoadPointer(&m.mapData)) // update pointer
index = key1 & mapData.andMask // update index key
}

newEntry := &hashMapEntry{
key1: key1,
key2: key2,
value: value,
}

sliceDataIndexPointer := unsafe.Pointer(uintptr(mapData.data) + uintptr(index*intSizeBytes))
atomic.StorePointer((*unsafe.Pointer)(sliceDataIndexPointer), unsafe.Pointer(newEntry))
atomic.AddUint64(&mapData.count, 1)
}

// Remove removes an element from the map.
func (m *HashMap) Remove(key1 uint64, key2 uint64) {
m.Lock()
defer m.Unlock()

_, exists := m.Get(key1, key2)
if !exists {
return
}

mapData := (*hashMapData)(atomic.LoadPointer(&m.mapData))
index := key1 & mapData.andMask

sliceDataIndexPointer := unsafe.Pointer(uintptr(mapData.data) + uintptr(index*intSizeBytes))
atomic.StorePointer((*unsafe.Pointer)(sliceDataIndexPointer), nil)
atomic.AddUint64(&mapData.count, ^uint64(0))
}

// Resize resizes the hashmap to a new size, gets rounded up to next power of 2
// Locking of the hashmap needs to be done outside of this function
func (m *HashMap) Resize(newSize uint64) {
newSize = RoundUpPower2(newSize)
newSlice := make([]*hashMapEntry, newSize)
header := (*reflect.SliceHeader)(unsafe.Pointer(&newSlice))

newMapData := &hashMapData{
andMask: newSize - 1,
data: unsafe.Pointer(header.Data),
size: newSize,
count: 0,
slice: newSlice,
}

mapData := (*hashMapData)(atomic.LoadPointer(&m.mapData))
if mapData != nil { // copy hashmap contents to new slice with longer key
newMapData.count = mapData.count
for _, entry := range mapData.slice {
if entry == nil {
continue
}

index := entry.key1 & mapData.andMask
newSlice[index] = entry
}
}

atomic.StorePointer(&m.mapData, unsafe.Pointer(newMapData))
}

// RoundUpPower2 rounds a number to the next power of 2.
func RoundUpPower2(i uint64) uint64 {
i--
i |= i >> 1
i |= i >> 2
i |= i >> 4
i |= i >> 8
i |= i >> 16
i |= i >> 32
i++
return i
}
106 changes: 106 additions & 0 deletions hashmap_test.go
@@ -0,0 +1,106 @@
package hashmap

import (
"strconv"
"testing"
)

type Animal struct {
name string
}

func TestMapCreation(t *testing.T) {
m := New()
if m == nil {
t.Error("map is null.")
}

if m.Count() != 0 {
t.Error("new map should be empty.")
}
}

func TestInsert(t *testing.T) {
m := New()
elephant := Animal{"elephant"}
monkey := Animal{"monkey"}

m.Set(1, 1, elephant)
m.Set(2, 2, monkey)

if m.Count() != 2 {
t.Error("map should contain exactly two elements.")
}
}

func TestGet(t *testing.T) {
m := New()

// Get a missing element.
val, ok := m.Get(1, 1)

if ok == true {
t.Error("ok should be false when item is missing from map.")
}

if val != nil {
t.Error("Missing values should return as null.")
}

elephant := Animal{"elephant"}
m.Set(2, 2, elephant)

// Retrieve inserted element.

tmp, ok := m.Get(2, 2)
elephant = tmp.(Animal) // Type assertion.

if ok == false {
t.Error("ok should be true for item stored within the map.")
}

if &elephant == nil {
t.Error("expecting an element, not null.")
}

if elephant.name != "elephant" {
t.Error("item was modified.")
}
}

func TestRemove(t *testing.T) {
m := New()

monkey := Animal{"monkey"}
m.Set(1, 1, monkey)

m.Remove(1, 1)

if m.Count() != 0 {
t.Error("Expecting count to be zero once item was removed.")
}

temp, ok := m.Get(1, 1)

if ok != false {
t.Error("Expecting ok to be false for missing items.")
}

if temp != nil {
t.Error("Expecting item to be nil after its removal.")
}

// Remove a none existing element.
m.Remove(2, 2)
}

func TestCount(t *testing.T) {
m := New()
for i := 0; i < 1024; i++ {
m.Set(uint64(i), uint64(i), Animal{strconv.Itoa(i)})
}

if m.Count() != 1024 {
t.Error("Expecting 100 element within map.")
}
}

0 comments on commit f64c8b0

Please sign in to comment.