Skip to content

Commit

Permalink
key: Make map[Key]interface{} always O(1) with some runtime hacks.
Browse files Browse the repository at this point in the history
This is based on a POC from Frank Somers.
This is a workaround for golang/go#283.

Before:
BenchmarkBigMapWithCompositeKeys-8      2000   2863709 ns/op

After:
BenchmarkBigMapWithCompositeKeys-8  10000000       573 ns/op

Unsurprisingly, the O(N) implementation is super slow with a map size of
10000 entries, compared to the O(1) implementation.

Change-Id: Ia9ed056218c59e77b64aceb59e23b95d37760e85
  • Loading branch information
tsuna committed May 2, 2016
1 parent daff3d2 commit 59d8141
Show file tree
Hide file tree
Showing 6 changed files with 170 additions and 118 deletions.
131 changes: 131 additions & 0 deletions key/composite.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// Copyright (C) 2016 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 key

import (
"encoding/json"
"fmt"
"reflect"
"unsafe"

"github.com/aristanetworks/goarista/areflect"
)

// composite allows storing a map[string]interface{} as a key in a Go map.
// This is useful when the key isn't a fixed data structure known at compile
// time but rather something generic, like a bag of key-value pairs.
// Go does not allow storing a map inside the key of a map, because maps are
// not comparable or hashable, and keys in maps must be both. This file is
// a hack specific to the 'gc' implementation of Go (which is the one most
// people use when they use Go), to bypass this check, by abusing reflection
// to override how Go compares composite for equality or how it's hashed.
// The values allowed in this map are only the types whitelisted in New() as
// well as map[Key]interface{}.
//
// See also https://github.com/golang/go/issues/283
type composite map[string]interface{}

func (k composite) Key() interface{} {
return map[string]interface{}(k)
}

func (k composite) String() string {
return stringify(k.Key())
}

func (k composite) GetFromMap(m map[Key]interface{}) (interface{}, bool) {
v, ok := m[k]
return v, ok
}

func (k composite) DeleteFromMap(m map[Key]interface{}) {
delete(m, k)
}

func (k composite) SetToMap(m map[Key]interface{}, value interface{}) {
m[k] = value
}

func (k composite) GoString() string {
return fmt.Sprintf("key.New(%#v)", k.Key())
}

func (k composite) MarshalJSON() ([]byte, error) {
return json.Marshal(k.Key())
}

func (k composite) Equal(other interface{}) bool {
o, ok := other.(Key)
if !ok {
return false
}
return keyEqual(k.Key(), o.Key())
}

func hashInterface(v interface{}) uintptr {
switch v := v.(type) {
case map[string]interface{}:
return hashMapString(v)
case map[Key]interface{}:
return hashMapKey(v)
default:
return _nilinterhash(v)
}
}

func hashMapString(m map[string]interface{}) uintptr {
h := uintptr(31 * (len(m) + 1))
for k, v := range m {
// Use addition so that the order of iteration doesn't matter.
h += _strhash(k)
h += hashInterface(v)
}
return h
}

func hashMapKey(m map[Key]interface{}) uintptr {
h := uintptr(31 * (len(m) + 1))
for k, v := range m {
// Use addition so that the order of iteration doesn't matter.
switch k := k.(type) {
case keyImpl:
h += _nilinterhash(k.key)
case composite:
h += hashMapString(k)
}
h += hashInterface(v)
}
return h
}

func hash(p unsafe.Pointer, seed uintptr) uintptr {
ck := *(*composite)(p)
return seed ^ hashMapString(ck)
}

func equal(a unsafe.Pointer, b unsafe.Pointer) bool {
return (*composite)(a).Equal(*(*composite)(b))
}

func init() {
typ := reflect.TypeOf(composite{})
alg := reflect.ValueOf(typ).Elem().FieldByName("alg").Elem()
// Pretty certain that doing this voids your warranty.
// This overwrites the typeAlg of either alg_NOEQ32 (on 32-bit platforms)
// or alg_NOEQ64 (on 64-bit platforms), which means that all unhashable
// types that were using this typeAlg are now suddenly hashable and will
// attempt to use our equal/hash functions, which will lead to undefined
// behaviors. But then these types shouldn't have been hashable in the
// first place, so no one should have attempted to use them as keys in a
// map. The compiler will emit an error if it catches someone trying to
// do this, but if they do it through a map that uses an interface type as
// the key, then the compiler can't catch it.
// To prevent this we could instead override the alg pointer in the type,
// but it's in a read-only data section in the binary (it's put there by
// dcommontype() in gc/reflect.go), so changing it is also not without
// perils. Basically: Here Be Dragons.
areflect.ForceExport(alg.FieldByName("hash")).Set(reflect.ValueOf(hash))
areflect.ForceExport(alg.FieldByName("equal")).Set(reflect.ValueOf(equal))
}
23 changes: 23 additions & 0 deletions key/hash.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (C) 2016 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 key

import "unsafe"

//go:noescape
//go:linkname strhash runtime.strhash
func strhash(a unsafe.Pointer, h uintptr) uintptr

func _strhash(s string) uintptr {
return strhash(unsafe.Pointer(&s), 0)
}

//go:noescape
//go:linkname nilinterhash runtime.nilinterhash
func nilinterhash(a unsafe.Pointer, h uintptr) uintptr

func _nilinterhash(v interface{}) uintptr {
return nilinterhash(unsafe.Pointer(&v), 0)
}
6 changes: 6 additions & 0 deletions key/issue15006.s
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Copyright (C) 2016 Arista Networks, Inc.
// Use of this source code is governed by the Apache License 2.0
// that can be found in the COPYING file.

// This file is intentionally empty.
// It's a workaround for https://github.com/golang/go/issues/15006
68 changes: 6 additions & 62 deletions key/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,6 @@ type Key interface {
String() string
Equal(other interface{}) bool

// IsHashable is true if this key is hashable and can be accessed in O(1) in a Go map.
// If false, then a O(N) lookup is required to find this key in a Go map.
// The only kind of unhashable key currently supported is map[string]interface{}.
IsHashable() bool

// Helper methods to manipulate maps keyed by `Key'.

// GetFromMap returns the value for the entry of this Key.
Expand All @@ -46,82 +41,35 @@ type keyImpl struct {
func New(intf interface{}) Key {
switch t := intf.(type) {
case map[string]interface{}:
intf = &t
return composite(t)
case int8, int16, int32, int64,
uint8, uint16, uint32, uint64,
float32, float64, string, bool,
value.Value:
return keyImpl{key: intf}
default:
panic(fmt.Sprintf("Invalid type for key: %T", intf))
}
return keyImpl{key: intf}
}

func (k keyImpl) Key() interface{} {
if m, ok := k.key.(*map[string]interface{}); ok {
return *m
}
return k.key
}

func (k keyImpl) String() string {
return stringify(k.key)
}

func isHashableMap(m map[Key]interface{}) bool {
for k := range m {
return k.IsHashable()
}
return true
}

func (k keyImpl) IsHashable() bool {
_, ok := k.key.(*map[string]interface{})
return !ok
}

func (k keyImpl) GetFromMap(m map[Key]interface{}) (interface{}, bool) {
if len(m) == 0 {
return nil, false
}
if isHashableMap(m) {
v, ok := m[k]
return v, ok
}
for key, value := range m {
if k.Equal(key) {
return value, true
}
}
return nil, false
v, ok := m[k]
return v, ok
}

func (k keyImpl) DeleteFromMap(m map[Key]interface{}) {
if len(m) == 0 {
return
}
if isHashableMap(m) {
delete(m, k)
return
}
for key := range m {
if k.Equal(key) {
delete(m, key)
return
}
}
delete(m, k)
}

func (k keyImpl) SetToMap(m map[Key]interface{}, value interface{}) {
if isHashableMap(m) {
m[k] = value
return
}
for key := range m {
if k.Equal(key) {
m[key] = value
return
}
}
m[k] = value
}

Expand All @@ -138,10 +86,6 @@ func (k keyImpl) Equal(other interface{}) bool {
if !ok {
return false
}
if m, ok := k.key.(*map[string]interface{}); ok {
m2, ok := o.Key().(map[string]interface{})
return ok && keyEqual(*m, m2)
}
return keyEqual(k.key, o.Key())
}

Expand Down
54 changes: 0 additions & 54 deletions key/key_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,60 +127,6 @@ func TestKeyEqual(t *testing.T) {
}
}

func TestIsHashable(t *testing.T) {
tests := []struct {
k interface{}
h bool
}{{
true,
true,
}, {
uint8(3),
true,
}, {
uint16(3),
true,
}, {
uint32(3),
true,
}, {
uint64(3),
true,
}, {
int8(3),
true,
}, {
int16(3),
true,
}, {
int32(3),
true,
}, {
int64(3),
true,
}, {
float32(3.2),
true,
}, {
float64(3.3),
true,
}, {
"foobar",
true,
}, {
map[string]interface{}{"foo": "bar"},
false,
}}

for _, tcase := range tests {
if New(tcase.k).IsHashable() != tcase.h {
t.Errorf("Wrong result for case:\nk: %#v",
tcase.k)

}
}
}

func TestGetFromMap(t *testing.T) {
tests := []struct {
k Key
Expand Down
6 changes: 4 additions & 2 deletions test/data_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -430,8 +430,10 @@ func getDeepEqualTests(t *testing.T) []deepEqualTestCase {
"a": map[key.Key]interface{}{key.New(map[string]interface{}{"k": 42}): true}}),
b: key.New(map[string]interface{}{
"a": map[key.Key]interface{}{key.New(map[string]interface{}{"k": 51}): true}}),
diff: `Comparable types are different: key.keyImpl{key:*map[string]interface {}` +
`{"a":<max_depth>}} vs key.keyImpl{key:*map[string]interface {}{"a":<max_depth>}}`,
diff: `Comparable types are different: key.composite{"a":map[key.Key]interface {}` +
`{key.composite{<max_depth>:<max_depth>}:true}} vs ` +
`key.composite{"a":map[key.Key]interface {}` +
`{key.composite{<max_depth>:<max_depth>}:true}}`,
}, {
a: code(42),
b: code(42),
Expand Down

0 comments on commit 59d8141

Please sign in to comment.