Skip to content

Commit

Permalink
add v2, a new version built with generics
Browse files Browse the repository at this point in the history
  • Loading branch information
edwingeng committed May 29, 2022
1 parent 86be1b9 commit 10e83bd
Show file tree
Hide file tree
Showing 7 changed files with 647 additions and 2 deletions.
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,18 @@ BenchmarkSerialxHashring/100-nodes 2535745 482.1 ns/
BenchmarkSerialxHashring/1000-nodes 2243271 549.6 ns/op
```

# Example
# Import

```go
// If golang version <= 1.17
import "github.com/edwingeng/doublejump"

// If golang version >= 1.18
import "github.com/edwingeng/doublejump/v2"
```

# Example
```go
h := NewHash()
for i := 0; i < 10; i++ {
h.Add(fmt.Sprintf("node%d", i))
Expand Down
2 changes: 1 addition & 1 deletion doublejump.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ func (this *Hash) Shrink() {
this.compact.shrink(this.loose.a)
}

// Get returns an object according to the key provided.
// Get returns an object according to the key provided, or nil if there is no object in the hash.
func (this *Hash) Get(key uint64) interface{} {
obj := this.loose.get(key)
switch obj {
Expand Down
30 changes: 30 additions & 0 deletions v2/benchmark/benchmark_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package benchmark

import (
"fmt"
"testing"

"github.com/edwingeng/doublejump/v2"
)

var (
g struct {
Ret string
}
)

func BenchmarkDoublejump(b *testing.B) {
for i := 10; i <= 1000; i *= 10 {
b.Run(fmt.Sprintf("%d-nodes", i), func(b *testing.B) {
h := doublejump.NewHash[string]()
for j := 0; j < i; j++ {
h.Add(fmt.Sprintf("node%d", j))
}

b.ResetTimer()
for j := 0; j < b.N; j++ {
g.Ret, _ = h.Get(uint64(j))
}
})
}
}
183 changes: 183 additions & 0 deletions v2/doublejump.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
// Package doublejump provides a revamped Google's jump consistent hash.
package doublejump

import (
"math/rand"

"github.com/dgryski/go-jump"
)

type optional[T comparable] struct {
b bool
v T
}

type looseHolder[T comparable] struct {
a []optional[T]
m map[T]int
f []int
}

func (holder *looseHolder[T]) add(obj T) {
if _, ok := holder.m[obj]; ok {
return
}

if n := len(holder.f); n == 0 {
holder.a = append(holder.a, optional[T]{v: obj, b: true})
holder.m[obj] = len(holder.a) - 1
} else {
idx := holder.f[n-1]
holder.f = holder.f[:n-1]
holder.a[idx] = optional[T]{v: obj, b: true}
holder.m[obj] = idx
}
}

func (holder *looseHolder[T]) remove(obj T) {
if idx, ok := holder.m[obj]; ok {
holder.a[idx] = optional[T]{}
holder.f = append(holder.f, idx)
delete(holder.m, obj)
}
}

func (holder *looseHolder[T]) get(key uint64) (T, bool) {
var defVal T
n := len(holder.a)
if n == 0 {
return defVal, false
}

h := jump.Hash(key, n)
if holder.a[h].b {
return holder.a[h].v, true
} else {
return defVal, false
}
}

func (holder *looseHolder[T]) shrink() {
if len(holder.f) == 0 {
return
}

var a []optional[T]
for _, opt := range holder.a {
if opt.b {
a = append(a, opt)
holder.m[opt.v] = len(a) - 1
}
}
holder.a = a
holder.f = nil
}

type compactHolder[T comparable] struct {
a []T
m map[T]int
}

func (holder *compactHolder[T]) add(obj T) {
if _, ok := holder.m[obj]; ok {
return
}

holder.a = append(holder.a, obj)
holder.m[obj] = len(holder.a) - 1
}

func (holder *compactHolder[T]) remove(obj T) {
if idx, ok := holder.m[obj]; ok {
newLen := len(holder.a) - 1
tail := holder.a[newLen]
holder.a[idx] = tail
holder.m[tail] = idx
var defVal T
holder.a[newLen] = defVal
holder.a = holder.a[:newLen]
delete(holder.m, obj)
}
}

func (holder *compactHolder[T]) get(key uint64) (T, bool) {
var defVal T
n := len(holder.a)
if n == 0 {
return defVal, false
}

h := jump.Hash(key*0xc6a4a7935bd1e995, n)
return holder.a[h], true
}

// Hash is a revamped Google's jump consistent hash. It overcomes the shortcoming of the
// original implementation - not being able to remove nodes.
type Hash[T comparable] struct {
loose looseHolder[T]
compact compactHolder[T]
}

// NewHash creates a new doublejump hash instance, which does NOT threadsafe.
func NewHash[T comparable]() *Hash[T] {
hash := &Hash[T]{}
hash.loose.m = make(map[T]int)
hash.compact.m = make(map[T]int)
return hash
}

// Add adds an object to the hash.
func (h *Hash[T]) Add(obj T) {
h.loose.add(obj)
h.compact.add(obj)
}

// Remove removes an object from the hash.
func (h *Hash[T]) Remove(obj T) {
h.loose.remove(obj)
h.compact.remove(obj)
}

// Len returns the number of objects in the hash.
func (h *Hash[T]) Len() int {
return len(h.compact.a)
}

// LooseLen returns the size of the inner loose object holder.
func (h *Hash[T]) LooseLen() int {
return len(h.loose.a)
}

// Shrink removes all empty slots from the hash.
func (h *Hash[T]) Shrink() {
h.loose.shrink()
}

// Get returns an object and a boolean value according to the key provided.
// If there is no object in the hash, ok is false.
func (h *Hash[T]) Get(key uint64) (obj T, ok bool) {
if obj, ok = h.loose.get(key); ok {
return obj, true
}
return h.compact.get(key)
}

// All returns all the objects in this Hash.
func (h *Hash[T]) All() []T {
n := len(h.compact.a)
if n == 0 {
return nil
}
all := make([]T, n)
copy(all, h.compact.a)
return all
}

// Random returns a random object.
func (h *Hash[T]) Random() (T, bool) {
n := len(h.compact.a)
if n > 0 {
return h.compact.a[rand.Intn(n)], true
}
return *new(T), false
}
Loading

0 comments on commit 10e83bd

Please sign in to comment.