From 4b6b742501233bf29bba7375e79aa014d3c9659b Mon Sep 17 00:00:00 2001 From: Aaron Beitch Date: Tue, 7 Jan 2020 17:37:31 -0800 Subject: [PATCH] hashmap: Implement a new map implementation Initial hashmap implementation that supports Get, Set, Delete. Implementation strongly influenced by: https://www.sebastiansylvan.com/post/robin-hood-hashing-should-be-your-default-hash-table-implementation/ Simple benchmarks show that it can be faster than go's map for map[string]interface{} keys. TODO: benchmarks that include deletes/tombstones. It is missing a couple nice properties of Go maps: 1) Go's map can be modified while being iterated. If additions are made to Hashmap while it is being iterated (Iter implementation still TODO), the iterator may encounter keys more than once or not at all. However, Hashmap iteration should behave fine if keys are deleted in the during iteration. 2) Go's map has a built in race detector that runs all the time. Change-Id: I93ba5fb9b6c5b6a71e2e75f5dbc18239464756a7 --- hashmap/hashmap.go | 169 ++++++++++++ hashmap/hashmap_test.go | 212 +++++++++++++++ key/mapof_test.go | 573 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 954 insertions(+) create mode 100644 hashmap/hashmap.go create mode 100644 hashmap/hashmap_test.go create mode 100644 key/mapof_test.go diff --git a/hashmap/hashmap.go b/hashmap/hashmap.go new file mode 100644 index 0000000..20ab178 --- /dev/null +++ b/hashmap/hashmap.go @@ -0,0 +1,169 @@ +// Copyright (c) 2020 Arista Networks, Inc. +// Use of this source code is governed by the Apache License 2.0 +// that can be found in the COPYING file. + +package hashmap + +import "math/bits" + +// Hashable represents the key for an entry in a Map that cannot natively be hashed +type Hashable interface { + Hash() uint64 + Equal(other interface{}) bool +} + +// Hashmap implements a hashmap +type Hashmap[K any, V any] struct { + seed uint64 + entries []entry[K, V] + length int + hash func(K) uint64 + equal func(K, K) bool +} + +func New[K any, V any](size uint, hash func(K) uint64, equal func(K, K) bool) *Hashmap[K, V] { + var entries []entry[K, V] + if size != 0 { + entries = make([]entry[K, V], 1<= int(float64(capacity)*0.9) { + m.resize(capacity * 2) + } + m.set(m.hash(k), k, v) +} + +func (m *Hashmap[K, V]) set(hash uint64, k K, v V) { + position := m.position(hash) + var distance int + for { + existing := &m.entries[position] + if !existing.occupied { + m.entries[position] = entry[K, V]{hash: hash, key: k, value: v, occupied: true} + m.length++ + return + } else if existing.hash == hash && m.equal(existing.key, k) { + existing.value = v + return + } + + existingDistance := position - m.position(existing.hash) + if existingDistance < 0 { + existingDistance += len(m.entries) + } + if distance > existingDistance { + // k is further from its desired position than existing.k, + // steal it's spot and find a new place for existing. + if existing.tombstone { + m.entries[position] = entry[K, V]{hash: hash, key: k, value: v, occupied: true} + m.length++ + return + } + hash, existing.hash = existing.hash, hash + k, existing.key = existing.key, k + v, existing.value = existing.value, v + distance = existingDistance + } else if distance == existingDistance && existing.tombstone { + m.entries[position] = entry[K, V]{hash: hash, key: k, value: v, occupied: true} + m.length++ + return + } + + distance++ + position = (position + 1) & m.mask() + } +} + +// Get gets the value associated with k +func (m *Hashmap[K, V]) Get(k K) (V, bool) { + ent := m.getRef(k) + if ent == nil { + var v V + return v, false + } + return ent.value, true +} + +func (m *Hashmap[K, V]) getRef(k K) *entry[K, V] { + hash := m.hash(k) + position := m.position(hash) + var distance int + for { + ent := &m.entries[position] + if !ent.occupied { + return nil + } + entDistance := position - m.position(ent.hash) + if entDistance < 0 { + entDistance += len(m.entries) + } + if distance > entDistance { + // Our distance has exceeded this entry's distance, we + // would have found our key by now if it was present. + return nil + } + if ent.hash == hash && m.equal(ent.key, k) { + return ent + } + distance++ + position = (position + 1) & m.mask() + } +} + +// Delete removes k from m +func (m *Hashmap[K, V]) Delete(k K) { + ent := m.getRef(k) + if ent == nil { + return + } + // Set the entry to a tombstone. We keep the entry's hash set, so + // that this entry's distance can still be calculated. + var ( + nilK K + nilV V + ) + ent.key = nilK + ent.value = nilV + ent.tombstone = true + m.length-- +} + +func (m *Hashmap[K, V]) resize(size int) { + oldEntries := m.entries + m.entries = make([]entry[K, V], size) + m.length = 0 + for _, ent := range oldEntries { + if !ent.occupied || ent.tombstone { + continue + } + m.set(ent.hash, ent.key, ent.value) + } +} diff --git a/hashmap/hashmap_test.go b/hashmap/hashmap_test.go new file mode 100644 index 0000000..2af8c11 --- /dev/null +++ b/hashmap/hashmap_test.go @@ -0,0 +1,212 @@ +// Copyright (c) 2019 Arista Networks, Inc. +// Use of this source code is governed by the Apache License 2.0 +// that can be found in the COPYING file. + +package hashmap + +import ( + "fmt" + "math/rand" + "strings" + "testing" + + "github.com/aristanetworks/goarista/key" +) + +type dumbHashable struct { + dumb interface{} +} + +func (d dumbHashable) Equal(other interface{}) bool { + if o, ok := other.(dumbHashable); ok { + return d.dumb == o.dumb + } + return false +} + +func (d dumbHashable) Hash() uint64 { + return 1234567890 +} + +func TestMapSetGet(t *testing.T) { + m := New[Hashable, any](0, + func(h Hashable) uint64 { return h.Hash() }, + func(x, y Hashable) bool { return x.Equal(y) }) + tests := []struct { + setkey interface{} + getkey interface{} + val interface{} + found bool + }{{ + setkey: dumbHashable{dumb: "hashable1"}, + getkey: dumbHashable{dumb: "hashable1"}, + val: 1, + found: true, + }, { + getkey: dumbHashable{dumb: "hashable2"}, + val: nil, + found: false, + }, { + setkey: dumbHashable{dumb: "hashable2"}, + getkey: dumbHashable{dumb: "hashable2"}, + val: 2, + found: true, + }, { + getkey: dumbHashable{dumb: "hashable42"}, + val: nil, + found: false, + }, { + setkey: key.New(map[string]interface{}{"a": int32(1)}), + getkey: key.New(map[string]interface{}{"a": int32(1)}), + val: "foo", + found: true, + }, { + getkey: key.New(map[string]interface{}{"a": int32(2)}), + val: nil, + found: false, + }, { + setkey: key.New(map[string]interface{}{"a": int32(2)}), + getkey: key.New(map[string]interface{}{"a": int32(2)}), + val: "bar", + found: true, + }} + for _, tcase := range tests { + if tcase.setkey != nil { + m.Set(tcase.setkey.(Hashable), tcase.val) + } + val, found := m.Get(tcase.getkey.(Hashable)) + if found != tcase.found { + t.Errorf("found is %t, but expected found %t", found, tcase.found) + } + if val != tcase.val { + t.Errorf("val is %v for key %v, but expected val %v", val, tcase.getkey, tcase.val) + } + } + t.Log(m.debug()) +} + +func BenchmarkMapGrow(b *testing.B) { + keys := make([]key.Key, 150) + for j := 0; j < len(keys); j++ { + keys[j] = key.New(map[string]interface{}{ + "foobar": 100, + "baz": j, + }) + } + b.Run("key.Map", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + m := key.NewMap() + for j := 0; j < len(keys); j++ { + m.Set(keys[j], "foobar") + } + if m.Len() != len(keys) { + b.Fatal(m) + } + } + }) + b.Run("Hashmap", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + m := New[Hashable, any](0, + func(h Hashable) uint64 { return h.Hash() }, + func(x, y Hashable) bool { return x.Equal(y) }) + for j := 0; j < len(keys); j++ { + m.Set(keys[j].(Hashable), "foobar") + } + if m.Len() != len(keys) { + b.Fatal(m) + } + } + }) + b.Run("Hashmap-presize", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + m := New[Hashable, any](150, + func(h Hashable) uint64 { return h.Hash() }, + func(x, y Hashable) bool { return x.Equal(y) }) + for j := 0; j < len(keys); j++ { + m.Set(keys[j].(Hashable), "foobar") + } + if m.Len() != len(keys) { + b.Fatal(m) + } + } + }) +} + +func BenchmarkMapGet(b *testing.B) { + keys := make([]key.Key, 150) + for j := 0; j < len(keys); j++ { + keys[j] = key.New(map[string]interface{}{ + "foobar": 100, + "baz": j, + }) + } + keysRandomOrder := make([]key.Key, len(keys)) + copy(keysRandomOrder, keys) + rand.Shuffle(len(keysRandomOrder), func(i, j int) { + keysRandomOrder[i], keysRandomOrder[j] = keysRandomOrder[j], keysRandomOrder[i] + }) + b.Run("key.Map", func(b *testing.B) { + m := key.NewMap() + for j := 0; j < len(keys); j++ { + m.Set(keys[j], "foobar") + } + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + for _, k := range keysRandomOrder { + _, ok := m.Get(k) + if !ok { + b.Fatal("didn't find key") + } + } + } + }) + b.Run("Hashmap", func(b *testing.B) { + m := New[Hashable, any](0, + func(h Hashable) uint64 { return h.Hash() }, + func(x, y Hashable) bool { return x.Equal(y) }) + for j := 0; j < len(keys); j++ { + m.Set(keys[j].(Hashable), "foobar") + } + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + for _, k := range keysRandomOrder { + _, ok := m.Get(k.(Hashable)) + if !ok { + b.Fatal("didn't find key") + } + } + } + }) +} + +func (m *Hashmap[K, V]) debug() string { + var buf strings.Builder + + for i, ent := range m.entries { + var ( + k string + distance int + ) + if !ent.occupied { + k = "" + } else { + if ent.tombstone { + k = "" + } else { + k = fmt.Sprint(ent.key) + } + distance = i - m.position(ent.hash) + if distance < 0 { + distance += len(m.entries) + } + } + fmt.Fprintf(&buf, "%d %d %s\n", i, distance, k) + } + + return buf.String() +} diff --git a/key/mapof_test.go b/key/mapof_test.go new file mode 100644 index 0000000..15f9e55 --- /dev/null +++ b/key/mapof_test.go @@ -0,0 +1,573 @@ +// Copyright (c) 2019 Arista Networks, Inc. +// Use of this source code is governed by the Apache License 2.0 +// that can be found in the COPYING file. + +//go:build go1.18 + +package key + +import ( + "errors" + "fmt" + "math/rand" + "strings" + "testing" +) + +func (m *MapOf[V]) debug() string { + var buf strings.Builder + for hash, entry := range m.custom { + fmt.Fprintf(&buf, "%d: ", hash) + first := true + _ = entryOfIter[V](&entry, func(k interface{}, v V) error { + if !first { + buf.WriteString(" -> ") + } + first = false + fmt.Fprintf(&buf, "{%v:%v}", k, v) + return nil + }) + buf.WriteByte('\n') + } + return buf.String() +} + +func TestMapOfEqual(t *testing.T) { + tests := []struct { + a *MapOf[any] + b *MapOf[any] + result bool + }{{ // empty + a: &MapOf[any]{}, + b: &MapOf[any]{normal: map[interface{}]interface{}{}, custom: map[uint64]entryOf[any]{}, length: 0}, + result: true, + }, { // length check + a: &MapOf[any]{}, + b: &MapOf[any]{normal: map[interface{}]interface{}{}, custom: map[uint64]entryOf[any]{}, length: 1}, + result: false, + }, { // map[string]interface{} + a: &MapOf[any]{normal: map[interface{}]interface{}{"a": 1}, length: 1}, + b: NewMapOf(KV[any]{"a", 1}), + result: true, + }, { // differing keys in normal + a: &MapOf[any]{normal: map[interface{}]interface{}{"a": "b"}, length: 1}, + b: NewMapOf[any](KV[any]{"b", "b"}), + result: false, + }, { // differing values in normal + a: &MapOf[any]{normal: map[interface{}]interface{}{"a": "b"}, length: 1}, + b: NewMapOf[any](KV[any]{"a", false}), + result: false, + }, { // multiple entries + a: &MapOf[any]{normal: map[interface{}]interface{}{"a": 1, "b": true}, length: 2}, + b: NewMapOf[any](KV[any]{"a", 1}, KV[any]{"b", true}), + result: true, + }, { // nested maps in values + a: &MapOf[any]{ + normal: map[interface{}]interface{}{"a": map[string]interface{}{"b": 3}}, + length: 1}, + b: NewMapOf[any](KV[any]{"a", map[string]interface{}{"b": 3}}), + result: true, + }, { // differing nested maps in values + a: &MapOf[any]{ + normal: map[interface{}]interface{}{"a": map[string]interface{}{"b": 3}}, + length: 1}, + b: NewMapOf[any](KV[any]{"a", map[string]interface{}{"b": 4}}), + result: false, + }, { // map with map as key + a: NewMapOf[any](KV[any]{New(map[string]interface{}{"a": 123}), "b"}), + b: NewMapOf[any](KV[any]{New(map[string]interface{}{"a": 123}), "b"}), + result: true, + }, { + a: NewMapOf[any](KV[any]{New(map[string]interface{}{"a": 123}), "a"}), + b: NewMapOf[any](KV[any]{New(map[string]interface{}{"a": 123}), "b"}), + result: false, + }, { + a: NewMapOf[any](KV[any]{New(map[string]interface{}{"a": 123}), "b"}), + b: NewMapOf[any](KV[any]{New(map[string]interface{}{"b": 123}), "b"}), + result: false, + }, { + a: NewMapOf[any](KV[any]{New(map[string]interface{}{"a": 1, "b": 2}), "c"}), + b: NewMapOf[any](KV[any]{New(map[string]interface{}{"a": 1, "b": 2}), "c"}), + result: true, + }, { // maps with keys that hash to same buckets in different order + a: NewMapOf[any]( + KV[any]{dumbHashable{dumb: "hashable1"}, 1}, + KV[any]{dumbHashable{dumb: "hashable2"}, 2}, + KV[any]{dumbHashable{dumb: "hashable3"}, 3}), + b: NewMapOf[any]( + KV[any]{dumbHashable{dumb: "hashable3"}, 3}, + KV[any]{dumbHashable{dumb: "hashable2"}, 2}, + KV[any]{dumbHashable{dumb: "hashable1"}, 1}), + result: true, + }, { // maps with map as value + a: &MapOf[any]{normal: map[interface{}]interface{}{ + "foo": &MapOf[any]{normal: map[interface{}]interface{}{"a": 1}, length: 1}}, length: 1}, + b: &MapOf[any]{normal: map[interface{}]interface{}{ + "foo": &MapOf[any]{normal: map[interface{}]interface{}{"a": 1}, length: 1}}, length: 1}, + result: true, + }} + + for _, tcase := range tests { + if tcase.a.Equal(tcase.b) != tcase.result { + t.Errorf("%v and %v are not equal", tcase.a, tcase.b) + } + } +} + +func TestMapOfEntry(t *testing.T) { + m := NewMapOf[any]() + verifyPresent := func(k, v interface{}) { + t.Helper() + if got, ok := m.Get(k); !ok || got != v { + t.Errorf("Get(%v): expected %v, got %v", k, v, got) + } + } + verifyAbsent := func(k interface{}) { + t.Helper() + if got, ok := m.Get(k); ok { + t.Errorf("Get(%v): expected not found, got %v", k, got) + } + } + + // create entry list 1 -> 2 -> 3 + for i := 1; i <= 3; i++ { + m.Set(dumbHashable{i}, 0) + if m.Len() != i { + t.Errorf("expected len %d, got %d", i, m.Len()) + } + verifyPresent(dumbHashable{i}, 0) + } + if len(m.custom) != 1 { + t.Errorf("expected custom map to have 1 entry list, got %d", len(m.custom)) + } + if m.Len() != 3 { + t.Errorf("expected len of 3, got %d", m.Len()) + } + + // overwrite list members + for i := 1; i <= 3; i++ { + m.Set(dumbHashable{i}, i) + verifyPresent(dumbHashable{i}, i) + } + if m.Len() != 3 { + t.Errorf("expected len of 3, got %d", m.Len()) + } + t.Log(m.debug()) + + // delete nonexistant member + m.Del(dumbHashable{4}) + if m.Len() != 3 { + t.Errorf("expected len of 3, got %d", m.Len()) + } + + // Check that iter works + i := 1 + _ = m.Iter(func(k, v interface{}) error { + exp := dumbHashable{i} + if k != exp { + t.Errorf("expected key %v got %v", exp, k) + } + if v != i { + t.Errorf("expected val %d got %v", i, v) + } + i++ + return nil + }) + + // delete middle of list + m.Del(dumbHashable{2}) + verifyPresent(dumbHashable{1}, 1) + verifyAbsent(dumbHashable{2}) + verifyPresent(dumbHashable{3}, 3) + if m.Len() != 2 { + t.Errorf("expected len of 2, got %d", m.Len()) + } + + // delete end of list + m.Del(dumbHashable{3}) + verifyPresent(dumbHashable{1}, 1) + verifyAbsent(dumbHashable{3}) + if m.Len() != 1 { + t.Errorf("expected len of 1, got %d", m.Len()) + } + + m.Set(dumbHashable{2}, 2) + // delete head of list with next member + m.Del(dumbHashable{1}) + verifyAbsent(dumbHashable{1}) + verifyPresent(dumbHashable{2}, 2) + if m.Len() != 1 { + t.Errorf("expected len of 1, got %d", m.Len()) + } + + // delete final list member + m.Del(dumbHashable{2}) + verifyAbsent(dumbHashable{2}) + if m.Len() != 0 { + t.Errorf("expected len of 0, got %d", m.Len()) + } + + if len(m.custom) != 0 { + t.Errorf("expected m.custom to be empty, but got len %d", len(m.custom)) + } +} + +func TestMapOfSetGet(t *testing.T) { + m := MapOf[interface{}]{} + tests := []struct { + setkey interface{} + getkey interface{} + val interface{} + found bool + }{{ + setkey: "a", + getkey: "a", + val: 1, + found: true, + }, { + setkey: "b", + getkey: "b", + val: 1, + found: true, + }, { + setkey: 42, + getkey: 42, + val: "foobar", + found: true, + }, { + setkey: dumbHashable{dumb: "hashable1"}, + getkey: dumbHashable{dumb: "hashable1"}, + val: 1, + found: true, + }, { + getkey: dumbHashable{dumb: "hashable2"}, + val: nil, + found: false, + }, { + setkey: dumbHashable{dumb: "hashable2"}, + getkey: dumbHashable{dumb: "hashable2"}, + val: 2, + found: true, + }, { + getkey: dumbHashable{dumb: "hashable42"}, + val: nil, + found: false, + }, { + setkey: New(map[string]interface{}{"a": 1}), + getkey: New(map[string]interface{}{"a": 1}), + val: "foo", + found: true, + }, { + getkey: New(map[string]interface{}{"a": 2}), + val: nil, + found: false, + }, { + setkey: New(map[string]interface{}{"a": 2}), + getkey: New(map[string]interface{}{"a": 2}), + val: "bar", + found: true, + }} + for _, tcase := range tests { + if tcase.setkey != nil { + m.Set(tcase.setkey, tcase.val) + } + val, found := m.Get(tcase.getkey) + if found != tcase.found { + t.Errorf("found is %t, but expected found %t", found, tcase.found) + } + if val != tcase.val { + t.Errorf("val is %v for key %v, but expected val %v", val, tcase.getkey, tcase.val) + } + } + +} + +func TestMapOfDel(t *testing.T) { + tests := []struct { + m *MapOf[interface{}] + del interface{} + exp *MapOf[interface{}] + }{{ + m: NewMapOf[interface{}](), + del: "a", + exp: NewMapOf[interface{}](), + }, { + m: NewMapOf[interface{}](), + del: New(map[string]interface{}{"a": 1}), + exp: NewMapOf[interface{}](), + }, { + m: NewMapOf(KV[interface{}]{"a", true}), + del: "a", + exp: NewMapOf[interface{}](), + }, { + m: NewMapOf(KV[interface{}]{dumbHashable{dumb: "hashable1"}, 42}), + del: dumbHashable{dumb: "hashable1"}, + exp: NewMapOf[interface{}](), + }} + + for _, tcase := range tests { + tcase.m.Del(tcase.del) + if !tcase.m.Equal(tcase.exp) { + t.Errorf("map %#v after del of element %v does not equal expected %#v", + tcase.m, tcase.del, tcase.exp) + } + } +} + +func TestMapOfIter(t *testing.T) { + tests := []struct { + m *MapOf[any] + elems []interface{} + }{{ + m: NewMapOf[any](), + elems: []interface{}{}, + }, { + m: NewMapOf[any](KV[any]{"a", true}), + elems: []interface{}{"a"}, + }, { + m: NewMapOf[any](KV[any]{dumbHashable{dumb: "hashable1"}, 42}), + elems: []interface{}{dumbHashable{dumb: "hashable1"}}, + }, { + m: NewMapOf[any]( + KV[any]{dumbHashable{dumb: "hashable2"}, 42}, + KV[any]{dumbHashable{dumb: "hashable3"}, 42}, + KV[any]{dumbHashable{dumb: "hashable4"}, 42}), + elems: []interface{}{dumbHashable{dumb: "hashable2"}, + dumbHashable{dumb: "hashable3"}, dumbHashable{dumb: "hashable4"}}, + }, { + m: NewMapOf[any]( + KV[any]{New(map[string]interface{}{"a": 123}), "b"}, + KV[any]{New(map[string]interface{}{"c": 456}), "d"}, + KV[any]{dumbHashable{dumb: "hashable1"}, 1}, + KV[any]{dumbHashable{dumb: "hashable2"}, 2}, + KV[any]{dumbHashable{dumb: "hashable3"}, 3}, + KV[any]{"x", true}, + KV[any]{"y", false}, + KV[any]{"z", nil}, + ), + elems: []interface{}{ + New(map[string]interface{}{"a": 123}), New(map[string]interface{}{"c": 456}), + dumbHashable{dumb: "hashable1"}, dumbHashable{dumb: "hashable2"}, + dumbHashable{dumb: "hashable3"}, "x", "y", "z"}, + }} + for _, tcase := range tests { + count := 0 + iterfunc := func(k, v interface{}) error { + if !contains(tcase.elems, k) { + return fmt.Errorf("map %#v should not contain element %v", tcase.m, k) + } + count++ + return nil + } + if err := tcase.m.Iter(iterfunc); err != nil { + t.Errorf("unexpected error %v", err) + } + + expectedcount := len(tcase.elems) + if count != expectedcount || tcase.m.length != expectedcount { + t.Errorf("found %d elements in map %#v when expected %d", count, tcase.m, expectedcount) + } + } +} + +func TestMapOfIterDel(t *testing.T) { + // Deleting from standard go maps while iterating is safe. Since a MapOf contains maps, + // deleting from a MapOf while iterating is also safe. + m := NewMapOf[any]( + KV[any]{"1", "2"}, + KV[any]{New("1"), "keyVal"}, + KV[any]{New(map[string]interface{}{"key1": "val1", "key2": 2}), "mapVal"}, + KV[any]{dumbHashable{dumb: "dumbkey"}, "dumbHashVal"}, + ) + if err := m.Iter(func(k, v interface{}) error { + m.Del(k) + if _, ok := m.Get(k); ok { + t.Errorf("key %v should not exist", k) + } + return nil + }); err != nil { + t.Error(err) + } + if m.Len() != 0 { + t.Errorf("map elements should all be deleted, but found %d elements", m.Len()) + } +} + +func TestMapOfKeys(t *testing.T) { + m := NewMapOf[any]( + KV[any]{"1", "2"}, + KV[any]{New("1"), "keyVal"}, + KV[any]{New(map[string]interface{}{"key1": "val1", "key2": 2}), "mapVal"}, + KV[any]{dumbHashable{dumb: "dumbkey"}, "dumbHashVal"}, + ) + if len(m.Keys()) != m.Len() { + t.Errorf("len(m.Keys()) %d != expected len(m) %d", len(m.Keys()), m.Len()) + } + for _, key := range m.Keys() { + if _, ok := m.Get(key); !ok { + t.Errorf("could not find key %s in map m %s", key, m) + } + } +} + +func TestMapOfValues(t *testing.T) { + m := NewMapOf[any]( + KV[any]{"1", "2"}, + KV[any]{New("1"), "keyVal"}, + KV[any]{New(map[string]interface{}{"key1": "val1", "key2": 2}), "mapVal"}, + KV[any]{dumbHashable{dumb: "dumbkey"}, "dumbHashVal"}, + ) + if len(m.Values()) != m.Len() { + t.Errorf("len(m.Values()) %d != expected len(m) %d", len(m.Values()), m.Len()) + } + for _, value := range m.Values() { + found := false + if err := m.Iter(func(k, v interface{}) error { + if v == value { + found = true + return errors.New("found") + } + return nil + }); err != nil { + if err.Error() == "found" { + found = true + } + } + if !found { + t.Errorf("could not find value %s in map m %s", value, m) + } + } +} + +func TestMapOfString(t *testing.T) { + for _, tc := range []struct { + m *MapOf[any] + s string + }{{ + m: NewMapOf[any](), + s: "key.MapOf[]", + }, { + m: NewMapOf[any](KV[any]{"1", "2"}), + s: "key.MapOf[1:2]", + }, { + m: NewMapOf[any](KV[any]{ + "3", "4"}, KV[any]{ + + "1", "2"}), + + s: "key.MapOf[1:2 3:4]", + }, { + m: NewMapOf[any](KV[any]{ + New(map[string]interface{}{"key1": uint32(1), "key2": uint32(2)}), "foobar"}, KV[any]{ + + New(map[string]interface{}{"key1": uint32(3), "key2": uint32(4)}), "bazquux"}), + + s: "key.MapOf[map[key1:1 key2:2]:foobar map[key1:3 key2:4]:bazquux]", + }} { + t.Run(tc.s, func(t *testing.T) { + out := tc.m.String() + if out != tc.s { + t.Errorf("expected %q got %q", tc.s, out) + } + }) + } +} + +func TestMapOfKeyString(t *testing.T) { + for _, tc := range []struct { + m *MapOf[any] + s string + }{{ + m: NewMapOf[any]( + KV[any]{New(uint32(42)), true}, + KV[any]{New("foo"), "bar"}, + KV[any]{New(map[string]interface{}{"hello": "world"}), "yolo"}, + KV[any]{New(map[string]interface{}{"key1": uint32(1), "key2": uint32(2)}), "foobar"}), + + s: "1_2=foobar_42=true_foo=bar_world=yolo", + }} { + t.Run(tc.s, func(t *testing.T) { + out := tc.m.KeyString() + if out != tc.s { + t.Errorf("expected %q got %q", tc.s, out) + } + }) + } +} + +func BenchmarkMapOfGrow(b *testing.B) { + keys := make([]Key, 150) + for j := 0; j < len(keys); j++ { + keys[j] = New(map[string]interface{}{ + "foobar": 100, + "baz": j, + }) + } + v := "foobar" + b.Run("key.MapOf[any]", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + m := NewMapOf[any]() + for j := 0; j < len(keys); j++ { + m.Set(keys[j], v) + } + } + }) + b.Run("key.MapOf[string]", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + m := NewMapOf[string]() + for j := 0; j < len(keys); j++ { + m.Set(keys[j], v) + } + } + }) +} + +func BenchmarkMapOfGet(b *testing.B) { + keys := make([]Key, 150) + for j := 0; j < len(keys); j++ { + keys[j] = New(map[string]interface{}{ + "foobar": 100, + "baz": j, + }) + } + keysRandomOrder := make([]Key, len(keys)) + copy(keysRandomOrder, keys) + rand.Shuffle(len(keysRandomOrder), func(i, j int) { + keysRandomOrder[i], keysRandomOrder[j] = keysRandomOrder[j], keysRandomOrder[i] + }) + v := "foobar" + b.Run("key.MapOf[any]", func(b *testing.B) { + m := NewMapOf[any]() + for j := 0; j < len(keys); j++ { + m.Set(keys[j], v) + } + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + for _, k := range keysRandomOrder { + _, ok := m.Get(k) + if !ok { + b.Fatal("didn't find key") + } + } + } + }) + b.Run("key.MapOf[string]", func(b *testing.B) { + m := NewMapOf[string]() + for j := 0; j < len(keys); j++ { + m.Set(keys[j], v) + } + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + for _, k := range keysRandomOrder { + _, ok := m.Get(k) + if !ok { + b.Fatal("didn't find key") + } + } + } + }) +}