From e00ce77b84cfb740e2aec25ff9587c7738b08bb1 Mon Sep 17 00:00:00 2001 From: cornelk Date: Mon, 29 Aug 2022 09:09:36 -0600 Subject: [PATCH] inline specialized hashing functions and update benchmarks --- README.md | 10 +- benchmarks/go.mod | 4 +- benchmarks/go.sum | 6 + hashmap.go | 32 ---- util_hash.go | 381 +++++++++++++++++++++++++++++----------------- 5 files changed, 254 insertions(+), 179 deletions(-) diff --git a/README.md b/README.md index 5b15fce..1e1c2f9 100644 --- a/README.md +++ b/README.md @@ -45,8 +45,8 @@ Reading from the hash map in a thread-safe way is nearly as fast as reading from in an unsafe way and twice as fast as Go's `sync.Map`: ``` -BenchmarkReadHashMapUint-8 1314156 955.6 ns/op -BenchmarkReadHaxMapUint-8 872134 1316 ns/op (can not handle hash 0 collisions) +BenchmarkReadHashMapUint-8 1601247 754.2 ns/op +BenchmarkReadHaxMapUint-8 1519165 788.3 ns/op BenchmarkReadGoMapUintUnsafe-8 1560886 762.8 ns/op BenchmarkReadGoMapUintMutex-8 42284 28232 ns/op BenchmarkReadGoSyncMapUint-8 468338 2672 ns/op @@ -54,9 +54,9 @@ BenchmarkReadGoSyncMapUint-8 468338 2672 ns/op Reading from the map while writes are happening: ``` -BenchmarkReadHashMapWithWritesUint-8 890938 1288 ns/op -BenchmarkReadGoMapWithWritesUintMutex-8 14290 86758 ns/op -BenchmarkReadGoSyncMapWithWritesUint-8 374464 3149 ns/op +BenchmarkReadHashMapWithWritesUint-8 1268134 941.6 ns/op +BenchmarkReadHaxMapWithWritesUint-8 1000000 1045 ns/op +BenchmarkReadGoSyncMapWithWritesUint-8 369918 3149 ns/op ``` Write performance without any concurrent reads: diff --git a/benchmarks/go.mod b/benchmarks/go.mod index 4da199a..dc2a2b4 100644 --- a/benchmarks/go.mod +++ b/benchmarks/go.mod @@ -5,8 +5,8 @@ go 1.19 replace github.com/cornelk/hashmap => ../ require ( - github.com/alphadose/haxmap v0.2.1-0.20220828165710-add810974d4f - github.com/cornelk/hashmap v1.0.5-0.20220828215932-152772b42884 + github.com/alphadose/haxmap v0.2.2 + github.com/cornelk/hashmap v1.0.6-0.20220829041708-517efe3afe16 ) require github.com/cespare/xxhash v1.1.0 // indirect diff --git a/benchmarks/go.sum b/benchmarks/go.sum index 7f27fde..6ec7b82 100644 --- a/benchmarks/go.sum +++ b/benchmarks/go.sum @@ -2,6 +2,12 @@ github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/alphadose/haxmap v0.2.1-0.20220828165710-add810974d4f h1:up6qKu3lIXQ4H3AyJovBy0Tp6Ug++aveneLrUv7TyNo= github.com/alphadose/haxmap v0.2.1-0.20220828165710-add810974d4f/go.mod h1:Fu37Wlmj7cR++vSLgRTu3fGy8wpjHGmMypM2aclkc1A= +github.com/alphadose/haxmap v0.2.1 h1:ioHHBBj0P/+TmlF1uMOGOB9/3abZWncHEsJCfUC2bC0= +github.com/alphadose/haxmap v0.2.1/go.mod h1:Fu37Wlmj7cR++vSLgRTu3fGy8wpjHGmMypM2aclkc1A= +github.com/alphadose/haxmap v0.2.2-0.20220829041207-9dcd5e601a18 h1:TKxqcdbOHjdE98DTsfiFzCJO3B8nBiDkrC73kK9QXtc= +github.com/alphadose/haxmap v0.2.2-0.20220829041207-9dcd5e601a18/go.mod h1:Fu37Wlmj7cR++vSLgRTu3fGy8wpjHGmMypM2aclkc1A= +github.com/alphadose/haxmap v0.2.2 h1:Dl74rdR4W00rw4qZj4itaEYsVSETn3hh4oMnjbD8AJM= +github.com/alphadose/haxmap v0.2.2/go.mod h1:Fu37Wlmj7cR++vSLgRTu3fGy8wpjHGmMypM2aclkc1A= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= diff --git a/hashmap.go b/hashmap.go index 8fc8850..2a343b8 100644 --- a/hashmap.go +++ b/hashmap.go @@ -249,38 +249,6 @@ func (m *HashMap[Key, Value]) allocate(newSize uintptr) { } } -// setDefaultHasher sets the default hasher depending on the key type. -func (m *HashMap[Key, Value]) setDefaultHasher() { - var key Key - switch any(key).(type) { - case string: - m.hasher = m.xxHashString - case int, uint, uintptr: - switch intSizeBytes { - case 2: - m.hasher = m.xxHashWord - case 4: - m.hasher = m.xxHashDword - case 8: - m.hasher = m.xxHashQword - default: - panic(fmt.Errorf("unsupported integer byte size %d", intSizeBytes)) - } - case int8, uint8: - m.hasher = m.xxHashByte - case int16, uint16: - m.hasher = m.xxHashWord - case int32, uint32, float32: - m.hasher = m.xxHashDword - case int64, uint64, float64, complex64: - m.hasher = m.xxHashQword - case complex128: - m.hasher = m.xxHashOword - default: - panic(fmt.Errorf("unsupported key type %T", key)) - } -} - func (m *HashMap[Key, Value]) isResizeNeeded(store *store[Key, Value], count uintptr) bool { l := uintptr(len(store.index)) // l can't be 0 as it gets initialized in New() fillRate := (count * 100) / l diff --git a/util_hash.go b/util_hash.go index 17c013a..47b836f 100644 --- a/util_hash.go +++ b/util_hash.go @@ -26,6 +26,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import ( + "fmt" "math/bits" "reflect" "unsafe" @@ -44,145 +45,245 @@ const ( // Specialized xxhash hash functions, optimized for the bit size of the key where available, // for all supported types beside string. -func (m *HashMap[Key, Value]) xxHashByte(key Key) uintptr { - bh := reflect.SliceHeader{ - Data: uintptr(unsafe.Pointer(&key)), - Len: 1, +// setDefaultHasher sets the default hasher depending on the key type. +// Inlines hashing as anonymous functions for performance improvements, other options like +// returning an anonymous functions from another function turned out to not be as performant. +// +//nolint:funlen, maintidx +func (m *HashMap[Key, Value]) setDefaultHasher() { + var key Key + switch any(key).(type) { + case string: + m.hasher = func(key Key) uintptr { + sh := (*reflect.StringHeader)(unsafe.Pointer(&key)) + bh := reflect.SliceHeader{ + Data: sh.Data, + Len: sh.Len, + Cap: sh.Len, // cap needs to be set, otherwise xxhash fails on ARM Macs + } + buf := *(*[]byte)(unsafe.Pointer(&bh)) + return uintptr(xxhash.Sum64(buf)) + } + + case int, uint, uintptr: + switch intSizeBytes { + case 2: + m.hasher = func(key Key) uintptr { + bh := reflect.SliceHeader{ + Data: uintptr(unsafe.Pointer(&key)), + Len: 2, + } + b := *(*[]byte)(unsafe.Pointer(&bh)) + + var h = prime5 + 2 + + h ^= uint64(b[0]) * prime5 + h = bits.RotateLeft64(h, 11) * prime1 + h ^= uint64(b[1]) * prime5 + h = bits.RotateLeft64(h, 11) * prime1 + + h ^= h >> 33 + h *= prime2 + h ^= h >> 29 + h *= prime3 + h ^= h >> 32 + + return uintptr(h) + } + + case 4: + m.hasher = func(key Key) uintptr { + bh := reflect.SliceHeader{ + Data: uintptr(unsafe.Pointer(&key)), + Len: 4, + } + b := *(*[]byte)(unsafe.Pointer(&bh)) + + var h = prime5 + 4 + h ^= (uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24) * prime1 + h = bits.RotateLeft64(h, 23)*prime2 + prime3 + + h ^= h >> 33 + h *= prime2 + h ^= h >> 29 + h *= prime3 + h ^= h >> 32 + + return uintptr(h) + } + + case 8: + m.hasher = func(key Key) uintptr { + bh := reflect.SliceHeader{ + Data: uintptr(unsafe.Pointer(&key)), + Len: 8, + } + b := *(*[]byte)(unsafe.Pointer(&bh)) + + var h = prime5 + 8 + + val := uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24 | + uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56 + + // inline round() + k1 := val * prime2 + k1 = bits.RotateLeft64(k1, 31) + k1 *= prime1 + + h ^= k1 + h = bits.RotateLeft64(h, 27)*prime1 + prime4 + + h ^= h >> 33 + h *= prime2 + h ^= h >> 29 + h *= prime3 + h ^= h >> 32 + + return uintptr(h) + } + + default: + panic(fmt.Errorf("unsupported integer byte size %d", intSizeBytes)) + } + + case int8, uint8: + m.hasher = func(key Key) uintptr { + bh := reflect.SliceHeader{ + Data: uintptr(unsafe.Pointer(&key)), + Len: 1, + } + b := *(*[]byte)(unsafe.Pointer(&bh)) + + var h = prime5 + 1 + h ^= uint64(b[0]) * prime5 + h = bits.RotateLeft64(h, 11) * prime1 + + h ^= h >> 33 + h *= prime2 + h ^= h >> 29 + h *= prime3 + h ^= h >> 32 + + return uintptr(h) + } + + case int16, uint16: + m.hasher = func(key Key) uintptr { + bh := reflect.SliceHeader{ + Data: uintptr(unsafe.Pointer(&key)), + Len: 2, + } + b := *(*[]byte)(unsafe.Pointer(&bh)) + + var h = prime5 + 2 + + h ^= uint64(b[0]) * prime5 + h = bits.RotateLeft64(h, 11) * prime1 + h ^= uint64(b[1]) * prime5 + h = bits.RotateLeft64(h, 11) * prime1 + + h ^= h >> 33 + h *= prime2 + h ^= h >> 29 + h *= prime3 + h ^= h >> 32 + + return uintptr(h) + } + + case int32, uint32, float32: + m.hasher = func(key Key) uintptr { + bh := reflect.SliceHeader{ + Data: uintptr(unsafe.Pointer(&key)), + Len: 4, + } + b := *(*[]byte)(unsafe.Pointer(&bh)) + + var h = prime5 + 4 + h ^= (uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24) * prime1 + h = bits.RotateLeft64(h, 23)*prime2 + prime3 + + h ^= h >> 33 + h *= prime2 + h ^= h >> 29 + h *= prime3 + h ^= h >> 32 + + return uintptr(h) + } + + case int64, uint64, float64, complex64: + m.hasher = func(key Key) uintptr { + bh := reflect.SliceHeader{ + Data: uintptr(unsafe.Pointer(&key)), + Len: 8, + } + b := *(*[]byte)(unsafe.Pointer(&bh)) + + var h = prime5 + 8 + + val := uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24 | + uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56 + + // inline round() + k1 := val * prime2 + k1 = bits.RotateLeft64(k1, 31) + k1 *= prime1 + + h ^= k1 + h = bits.RotateLeft64(h, 27)*prime1 + prime4 + + h ^= h >> 33 + h *= prime2 + h ^= h >> 29 + h *= prime3 + h ^= h >> 32 + + return uintptr(h) + } + + case complex128: + m.hasher = func(key Key) uintptr { + bh := reflect.SliceHeader{ + Data: uintptr(unsafe.Pointer(&key)), + Len: 16, + } + b := *(*[]byte)(unsafe.Pointer(&bh)) + + var h = prime5 + 16 + + val := uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24 | + uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56 + + // inline round() + k1 := val * prime2 + k1 = bits.RotateLeft64(k1, 31) + k1 *= prime1 + + h ^= k1 + h = bits.RotateLeft64(h, 27)*prime1 + prime4 + + val = uint64(b[8]) | uint64(b[9])<<8 | uint64(b[10])<<16 | uint64(b[11])<<24 | + uint64(b[12])<<32 | uint64(b[13])<<40 | uint64(b[14])<<48 | uint64(b[15])<<56 + + // inline round() + k1 = val * prime2 + k1 = bits.RotateLeft64(k1, 31) + k1 *= prime1 + + h ^= k1 + h = bits.RotateLeft64(h, 27)*prime1 + prime4 + + h ^= h >> 33 + h *= prime2 + h ^= h >> 29 + h *= prime3 + h ^= h >> 32 + + return uintptr(h) + } + + default: + panic(fmt.Errorf("unsupported key type %T", key)) } - b := *(*[]byte)(unsafe.Pointer(&bh)) - - var h = prime5 + 1 - h ^= uint64(b[0]) * prime5 - h = bits.RotateLeft64(h, 11) * prime1 - - h ^= h >> 33 - h *= prime2 - h ^= h >> 29 - h *= prime3 - h ^= h >> 32 - - return uintptr(h) -} - -func (m *HashMap[Key, Value]) xxHashWord(key Key) uintptr { - bh := reflect.SliceHeader{ - Data: uintptr(unsafe.Pointer(&key)), - Len: 2, - } - b := *(*[]byte)(unsafe.Pointer(&bh)) - - var h = prime5 + 2 - - h ^= uint64(b[0]) * prime5 - h = bits.RotateLeft64(h, 11) * prime1 - h ^= uint64(b[1]) * prime5 - h = bits.RotateLeft64(h, 11) * prime1 - - h ^= h >> 33 - h *= prime2 - h ^= h >> 29 - h *= prime3 - h ^= h >> 32 - - return uintptr(h) -} - -func (m *HashMap[Key, Value]) xxHashDword(key Key) uintptr { - bh := reflect.SliceHeader{ - Data: uintptr(unsafe.Pointer(&key)), - Len: 4, - } - b := *(*[]byte)(unsafe.Pointer(&bh)) - - var h = prime5 + 4 - h ^= (uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24) * prime1 - h = bits.RotateLeft64(h, 23)*prime2 + prime3 - - h ^= h >> 33 - h *= prime2 - h ^= h >> 29 - h *= prime3 - h ^= h >> 32 - - return uintptr(h) -} - -func (m *HashMap[Key, Value]) xxHashQword(key Key) uintptr { - bh := reflect.SliceHeader{ - Data: uintptr(unsafe.Pointer(&key)), - Len: 8, - } - b := *(*[]byte)(unsafe.Pointer(&bh)) - - var h = prime5 + 8 - - val := uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24 | - uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56 - - // inline round() - k1 := val * prime2 - k1 = bits.RotateLeft64(k1, 31) - k1 *= prime1 - - h ^= k1 - h = bits.RotateLeft64(h, 27)*prime1 + prime4 - - h ^= h >> 33 - h *= prime2 - h ^= h >> 29 - h *= prime3 - h ^= h >> 32 - - return uintptr(h) -} - -func (m *HashMap[Key, Value]) xxHashOword(key Key) uintptr { - bh := reflect.SliceHeader{ - Data: uintptr(unsafe.Pointer(&key)), - Len: 16, - } - b := *(*[]byte)(unsafe.Pointer(&bh)) - - var h = prime5 + 16 - - val := uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24 | - uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56 - - // inline round() - k1 := val * prime2 - k1 = bits.RotateLeft64(k1, 31) - k1 *= prime1 - - h ^= k1 - h = bits.RotateLeft64(h, 27)*prime1 + prime4 - - val = uint64(b[8]) | uint64(b[9])<<8 | uint64(b[10])<<16 | uint64(b[11])<<24 | - uint64(b[12])<<32 | uint64(b[13])<<40 | uint64(b[14])<<48 | uint64(b[15])<<56 - - // inline round() - k1 = val * prime2 - k1 = bits.RotateLeft64(k1, 31) - k1 *= prime1 - - h ^= k1 - h = bits.RotateLeft64(h, 27)*prime1 + prime4 - - h ^= h >> 33 - h *= prime2 - h ^= h >> 29 - h *= prime3 - h ^= h >> 32 - - return uintptr(h) -} - -func (m *HashMap[Key, Value]) xxHashString(key Key) uintptr { - sh := (*reflect.StringHeader)(unsafe.Pointer(&key)) - bh := reflect.SliceHeader{ - Data: sh.Data, - Len: sh.Len, - Cap: sh.Len, // cap needs to be set, otherwise xxhash fails on ARM Macs - } - buf := *(*[]byte)(unsafe.Pointer(&bh)) - return uintptr(xxhash.Sum64(buf)) }