Files
tinygo/src/runtime/hashmap.go
T
Jake Bailey 18033ebc36 compiler, runtime, reflect: generate type-specific hash/equal (#5359)
* compiler, runtime, reflect: generate type-specific hash/equal for composite map keys

For map keys that are not trivially binary-comparable, the compiler now
generates type-specific hash and equal functions as LLVM IR instead of
going through the interface+reflection path. This covers comparable
types: strings, floats, complex numbers, interfaces, channels, and
composites containing any mix of these.

Previously, maps with composite keys containing strings or floats
converted the key to interface{}, hashed via reflection, and compared
through interface equality. Now the compiler walks struct fields and
array elements directly, dispatching to the right runtime helper for
each field type and storing keys at their actual type.

Struct keys are always handled field-by-field so padding bytes do not
affect equality or hashing. Blank fields are ignored, matching Go
equality. Generated hash/equal function names use canonical underlying
type structure so structurally identical key types can share generated
functions. Padding zeroing before map operations is no longer needed
because structs no longer use the binary key path.

Also fix reflect map iteration for interface-keyed maps: MapIter.Key
returns an interface Value for map[interface{}] keys instead of
unpacking to the concrete key kind.

* compiler: generate loops for array map key hash/equal

Previously, array key hash and equal functions were unrolled at compile
time, generating one block of IR per element. For large arrays like
[1000]int inside a struct with non-binary fields, this caused code
explosion.

Now, binary-element arrays dispatch directly to hash32/memequal for the
whole array. Non-binary-element arrays generate an LLVM IR loop. The
equal loop short-circuits on the first mismatch.

Small arrays are still unrolled instead of looping, keeping the simple
cases compact.

* reflect: fix at-runtime map issues from review, and more found locally

Maps created through reflect.MakeMap need hash/equal behavior that
matches compiler-created maps. Add hashmapMakeReflect for composite key
types, using runtime closures that reconstruct interface{} values from
raw key bytes and delegate to the interface hash and equality paths.

Interface-keyed maps are already stored as interface values, so use the
existing interface hash/equal helpers directly for those. This keeps
reflect insert, lookup, delete, and compiled lookup paths consistent.

Also fix addressable small values used as interface map keys or
interface map values. loadSmallValue puts small indirect values back in
the pointer-sized interface data field the same way valueInterfaceUnsafe
does.

* compiler, interp, reflect: fix pointer map literals; remove interface fallback

Package-level map literals with pointer keys (both *T and
unsafe.Pointer) crash the compiler: the interp pass panics when trying
to hash pointer data as raw bytes, because pointer values in the interp
memory model are symbolic identities that do not fit in a byte.

Fix this by setting a recoverable error flag instead of panicking. The
interp detects the error after each instruction and defers the map
insert to runtime init code, where real addresses are available for
hashing. This matches how the interp already handles other operations
it cannot evaluate at compile time.

With this fix, unsafe.Pointer can also be classified as a binary map
key, which was the last type requiring the interface-based fallback.
Since all comparable types now use either the binary or the
compiler-generated hash/equal path, remove the interface fallback from
the compiler and reflect packages.

* compiler, transform: always pass hash/equal function pointers to hashmapMakeGeneric

The compiler now always resolves the hash and equal functions at compile
time and passes them directly to hashmapMakeGeneric, instead of passing
an algorithm enum to hashmapMake and resolving at runtime. For string
keys, the runtime hashmapStringPtrHash/hashmapStringEqual functions are
referenced directly. For binary keys, hash32/memequal are referenced.

The old hashmapMake with alg enum is retained for reflect, which still
needs runtime resolution when creating maps dynamically.

The OptimizeMaps transform pass is updated to handle both hashmapMake
and hashmapMakeGeneric, and to recognize hashmapGenericSet in addition
to hashmapBinarySet and hashmapStringSet. The now-unused
hashmapCanGenerateHashEqual helper is removed.

* runtime: store large map keys and values indirectly

When a map key or value exceeds 128 bytes, the bucket now stores a
pointer to separately allocated memory instead of the data inline. This
matches Go's MapMaxKeyBytes/MapMaxElemBytes threshold and prevents
bucket sizes from exploding for large key/value types.

For example, map[[256]byte]int previously used 2128 bytes per bucket
(16 header + 256*8 keys + 8*8 values); now it uses 144 bytes per bucket
(16 header + 8*8 pointers + 8*8 values).

The indirection is fully encapsulated in the runtime via helper
functions. Store the computed key and value slot sizes on the hashmap so
all runtime and reflect paths use the same bucket layout, including
non-indirect keys and values.

Add big-key golden coverage and benchmarks. Make the benchmark vary
enough key bytes to exercise hashing.
2026-05-18 13:31:27 +02:00

833 lines
25 KiB
Go

package runtime
// This is a hashmap implementation for the map[T]T type.
// It is very roughly based on the implementation of the Go hashmap:
//
// https://golang.org/src/runtime/map.go
import (
"internal/reflectlite"
"tinygo"
"unsafe"
)
// The underlying hashmap structure for Go.
type hashmap struct {
buckets unsafe.Pointer // pointer to array of buckets
seed uintptr
count uintptr
keySize uintptr
valueSize uintptr
keySlotSize uintptr // == keySize, or sizeof(ptr) if indirect
valueSlotSize uintptr // == valueSize, or sizeof(ptr) if indirect
bucketBits uint8
flags uint8
keyEqual func(x, y unsafe.Pointer, n uintptr) bool
keyHash func(key unsafe.Pointer, size, seed uintptr) uint32
}
const (
hashmapMaxKeySize = 128
hashmapMaxValueSize = 128
hashmapFlagIndirectKey = 1 << 0
hashmapFlagIndirectValue = 1 << 1
)
// A hashmap bucket. A bucket is a container of 8 key/value pairs: first the
// following two entries, then the 8 keys, then the 8 values. This somewhat odd
// ordering is to make sure the keys and values are well aligned when one of
// them is smaller than the system word size.
type hashmapBucket struct {
tophash [8]uint8
next *hashmapBucket // next bucket (if there are more than 8 in a chain)
// Followed by the actual keys, and then the actual values. These are
// allocated but as they're of variable size they can't be shown here.
}
// hashmapBucketHeaderSize is the offset in bytes from the start of a bucket to
// the first key, aligned to 8 bytes. This ensures that keys requiring 8-byte
// alignment (float64, complex128, uint64 on strict-alignment architectures
// like MIPS) are properly aligned in the bucket.
const hashmapBucketHeaderSize = (unsafe.Sizeof(hashmapBucket{}) + 7) &^ 7
// hashmapKeySlotSize returns the size of a key slot in the bucket. For indirect
// keys, this is the pointer size; otherwise the actual key size.
//
//go:inline
func hashmapKeySlotSize(m *hashmap) uintptr {
return m.keySlotSize
}
// hashmapValueSlotSize returns the size of a value slot in the bucket.
//
//go:inline
func hashmapValueSlotSize(m *hashmap) uintptr {
return m.valueSlotSize
}
// hashmapSlotKeyData returns a pointer to the actual key data for a given slot.
// For indirect keys, the slot contains a pointer that must be dereferenced.
//
//go:inline
func hashmapSlotKeyData(m *hashmap, slotKey unsafe.Pointer) unsafe.Pointer {
if m.flags&hashmapFlagIndirectKey != 0 {
return *(*unsafe.Pointer)(slotKey)
}
return slotKey
}
// hashmapSlotValueData returns a pointer to the actual value data for a given slot.
//
//go:inline
func hashmapSlotValueData(m *hashmap, slotValue unsafe.Pointer) unsafe.Pointer {
if m.flags&hashmapFlagIndirectValue != 0 {
return *(*unsafe.Pointer)(slotValue)
}
return slotValue
}
type hashmapIterator struct {
buckets unsafe.Pointer // pointer to array of hashapBuckets
numBuckets uintptr // length of buckets array
bucketNumber uintptr // current index into buckets array
startBucket uintptr // starting location for iterator
bucket *hashmapBucket // current bucket in chain
bucketIndex uint8 // current index into bucket
startIndex uint8 // starting bucket index for iterator
wrapped bool // true if the iterator has wrapped
}
func hashmapNewIterator() unsafe.Pointer {
return unsafe.Pointer(new(hashmapIterator))
}
// Get the topmost 8 bits of the hash, without using a special value (like 0).
func hashmapTopHash(hash uint32) uint8 {
tophash := uint8(hash >> 24)
if tophash < 1 {
// 0 means empty slot, so make it bigger.
tophash += 1
}
return tophash
}
// Create a new hashmap with the given keySize and valueSize.
func hashmapMake(keySize, valueSize uintptr, sizeHint uintptr, alg uint8) *hashmap {
bucketBits := uint8(0)
for hashmapHasSpaceToGrow(bucketBits) && hashmapOverLoadFactor(sizeHint, bucketBits) {
bucketBits++
}
var flags uint8
keySlotSize := keySize
if keySize > hashmapMaxKeySize {
flags |= hashmapFlagIndirectKey
keySlotSize = unsafe.Sizeof(unsafe.Pointer(nil))
}
valueSlotSize := valueSize
if valueSize > hashmapMaxValueSize {
flags |= hashmapFlagIndirectValue
valueSlotSize = unsafe.Sizeof(unsafe.Pointer(nil))
}
bucketBufSize := hashmapBucketHeaderSize + keySlotSize*8 + valueSlotSize*8
buckets := alloc(bucketBufSize*(1<<bucketBits), nil)
keyHash := hashmapKeyHashAlg(tinygo.HashmapAlgorithm(alg))
keyEqual := hashmapKeyEqualAlg(tinygo.HashmapAlgorithm(alg))
return &hashmap{
buckets: buckets,
seed: uintptr(fastrand()),
keySize: keySize,
valueSize: valueSize,
keySlotSize: keySlotSize,
valueSlotSize: valueSlotSize,
bucketBits: bucketBits,
flags: flags,
keyEqual: keyEqual,
keyHash: keyHash,
}
}
// Remove all entries from the map, without actually deallocating the space for
// it. This is used for the clear builtin, and can be used to reuse a map (to
// avoid extra heap allocations).
func hashmapClear(m *hashmap) {
if m == nil {
// Nothing to do. According to the spec:
// > If the map or slice is nil, clear is a no-op.
return
}
m.count = 0
numBuckets := uintptr(1) << m.bucketBits
bucketSize := hashmapBucketSize(m)
for i := uintptr(0); i < numBuckets; i++ {
bucket := hashmapBucketAddr(m, m.buckets, i)
for bucket != nil {
// Clear the tophash, to mark these keys/values as removed.
bucket.tophash = [8]uint8{}
// Clear the keys and values in the bucket so that the GC won't pin
// these allocations.
memzero(unsafe.Add(unsafe.Pointer(bucket), unsafe.Sizeof(hashmapBucket{})), bucketSize-unsafe.Sizeof(hashmapBucket{}))
// Move on to the next bucket in the chain.
bucket = bucket.next
}
}
}
func hashmapKeyEqualAlg(alg tinygo.HashmapAlgorithm) func(x, y unsafe.Pointer, n uintptr) bool {
switch alg {
case tinygo.HashmapAlgorithmBinary:
return memequal
case tinygo.HashmapAlgorithmString:
return hashmapStringEqual
default:
// compiler bug :(
return nil
}
}
func hashmapKeyHashAlg(alg tinygo.HashmapAlgorithm) func(key unsafe.Pointer, n, seed uintptr) uint32 {
switch alg {
case tinygo.HashmapAlgorithmBinary:
return hash32
case tinygo.HashmapAlgorithmString:
return hashmapStringPtrHash
default:
// compiler bug :(
return nil
}
}
func hashmapHasSpaceToGrow(bucketBits uint8) bool {
// Over this limit, we're likely to overflow uintptrs during calculations
// or numbers of hash elements. Don't allow any more growth.
// With 29 bits, this is 2^32 elements anyway.
return bucketBits <= uint8((unsafe.Sizeof(uintptr(0))*8)-3)
}
func hashmapOverLoadFactor(n uintptr, bucketBits uint8) bool {
// "maximum" number of elements is 0.75 * buckets * elements per bucket
// to avoid overflow, this is calculated as
// max = 3 * (1/4 * buckets * elements per bucket)
// = 3 * (buckets * (elements per bucket)/4)
// = 3 * (buckets * (8/4)
// = 3 * (buckets * 2)
// = 6 * buckets
max := (uintptr(6) << bucketBits)
return n > max
}
// Return the number of entries in this hashmap, called from the len builtin.
// A nil hashmap is defined as having length 0.
//
//go:inline
func hashmapLen(m *hashmap) int {
if m == nil {
return 0
}
return int(m.count)
}
//go:inline
func hashmapBucketSize(m *hashmap) uintptr {
return hashmapBucketHeaderSize + hashmapKeySlotSize(m)*8 + hashmapValueSlotSize(m)*8
}
//go:inline
func hashmapBucketAddr(m *hashmap, buckets unsafe.Pointer, n uintptr) *hashmapBucket {
bucketSize := hashmapBucketSize(m)
bucket := (*hashmapBucket)(unsafe.Add(buckets, bucketSize*n))
return bucket
}
//go:inline
func hashmapBucketAddrForHash(m *hashmap, hash uint32) *hashmapBucket {
numBuckets := uintptr(1) << m.bucketBits
bucketNumber := (uintptr(hash) & (numBuckets - 1))
return hashmapBucketAddr(m, m.buckets, bucketNumber)
}
//go:inline
func hashmapSlotKey(m *hashmap, bucket *hashmapBucket, slot uint8) unsafe.Pointer {
slotKeyOffset := hashmapBucketHeaderSize + hashmapKeySlotSize(m)*uintptr(slot)
return unsafe.Add(unsafe.Pointer(bucket), slotKeyOffset)
}
//go:inline
func hashmapSlotValue(m *hashmap, bucket *hashmapBucket, slot uint8) unsafe.Pointer {
slotValueOffset := hashmapBucketHeaderSize + hashmapKeySlotSize(m)*8 + hashmapValueSlotSize(m)*uintptr(slot)
return unsafe.Add(unsafe.Pointer(bucket), slotValueOffset)
}
// Set a specified key to a given value. Grow the map if necessary.
//
//go:nobounds
func hashmapSet(m *hashmap, key unsafe.Pointer, value unsafe.Pointer, hash uint32) {
if hashmapHasSpaceToGrow(m.bucketBits) && hashmapOverLoadFactor(m.count, m.bucketBits) {
hashmapGrow(m)
// seed changed when we grew; rehash key with new seed
hash = m.keyHash(key, m.keySize, m.seed)
}
tophash := hashmapTopHash(hash)
bucket := hashmapBucketAddrForHash(m, hash)
var lastBucket *hashmapBucket
// See whether the key already exists somewhere.
var emptySlotKey unsafe.Pointer
var emptySlotValue unsafe.Pointer
var emptySlotTophash *byte
for bucket != nil {
for i := uint8(0); i < 8; i++ {
slotKey := hashmapSlotKey(m, bucket, i)
slotValue := hashmapSlotValue(m, bucket, i)
if bucket.tophash[i] == 0 && emptySlotKey == nil {
// Found an empty slot, store it for if we couldn't find an
// existing slot.
emptySlotKey = slotKey
emptySlotValue = slotValue
emptySlotTophash = &bucket.tophash[i]
}
if bucket.tophash[i] == tophash {
// Could be an existing key that's the same.
if m.keyEqual(key, hashmapSlotKeyData(m, slotKey), m.keySize) {
// found same key, replace the value
hashmapStoreValue(m, slotValue, value)
return
}
}
}
lastBucket = bucket
bucket = bucket.next
}
if emptySlotKey == nil {
// Add a new bucket to the bucket chain.
// TODO: rebalance if necessary to avoid O(n) insert and lookup time.
lastBucket.next = (*hashmapBucket)(hashmapInsertIntoNewBucket(m, key, value, tophash))
return
}
m.count++
hashmapStoreKey(m, emptySlotKey, key)
hashmapStoreValue(m, emptySlotValue, value)
*emptySlotTophash = tophash
}
// hashmapStoreKey stores a key into a bucket slot, allocating backing storage
// if the key is indirect (first insert) or copying into it (shouldn't happen
// for keys, but handles it correctly).
//
//go:inline
func hashmapStoreKey(m *hashmap, slotKey, key unsafe.Pointer) {
if m.flags&hashmapFlagIndirectKey != 0 {
p := alloc(m.keySize, nil)
memcpy(p, key, m.keySize)
*(*unsafe.Pointer)(slotKey) = p
} else {
memcpy(slotKey, key, m.keySize)
}
}
// hashmapStoreValue stores a value into a bucket slot. For indirect values,
// it allocates backing storage on first insert or copies into the existing
// backing on overwrite.
//
//go:inline
func hashmapStoreValue(m *hashmap, slotValue, value unsafe.Pointer) {
if m.flags&hashmapFlagIndirectValue != 0 {
p := *(*unsafe.Pointer)(slotValue)
if p == nil {
// First insert: allocate backing storage.
p = alloc(m.valueSize, nil)
*(*unsafe.Pointer)(slotValue) = p
}
memcpy(p, value, m.valueSize)
} else {
memcpy(slotValue, value, m.valueSize)
}
}
// hashmapInsertIntoNewBucket creates a new bucket, inserts the given key and
// value into the bucket, and returns a pointer to this bucket.
func hashmapInsertIntoNewBucket(m *hashmap, key, value unsafe.Pointer, tophash uint8) *hashmapBucket {
bucketBufSize := hashmapBucketSize(m)
bucketBuf := alloc(bucketBufSize, nil)
bucket := (*hashmapBucket)(bucketBuf)
// Insert into the first slot, which is empty as it has just been allocated.
slotKey := hashmapSlotKey(m, bucket, 0)
slotValue := hashmapSlotValue(m, bucket, 0)
m.count++
hashmapStoreKey(m, slotKey, key)
hashmapStoreValue(m, slotValue, value)
bucket.tophash[0] = tophash
return bucket
}
func hashmapGrow(m *hashmap) {
// allocate our new buckets twice as big
n := hashmapCopy(m, m.bucketBits+1)
*m = n
}
//go:linkname hashmapClone maps.clone
func hashmapClone(intf _interface) _interface {
typ, val := decomposeInterface(intf)
m := (*hashmap)(val)
n := hashmapCopy(m, m.bucketBits)
return composeInterface(typ, unsafe.Pointer(&n))
}
func hashmapCopy(m *hashmap, sizeBits uint8) hashmap {
// clone map as empty
n := *m
n.count = 0
n.seed = uintptr(fastrand())
n.bucketBits = sizeBits
numBuckets := uintptr(1) << n.bucketBits
bucketBufSize := hashmapBucketSize(m)
n.buckets = alloc(bucketBufSize*numBuckets, nil)
// use a hashmap iterator to go through the old map
var it hashmapIterator
var key = alloc(m.keySize, nil)
var value = alloc(m.valueSize, nil)
for hashmapNext(m, &it, key, value) {
h := n.keyHash(key, uintptr(n.keySize), n.seed)
hashmapSet(&n, key, value, h)
}
return n
}
// Get the value of a specified key, or zero the value if not found.
//
//go:nobounds
func hashmapGet(m *hashmap, key, value unsafe.Pointer, valueSize uintptr, hash uint32) bool {
if m == nil {
// Getting a value out of a nil map is valid. From the spec:
// > if the map is nil or does not contain such an entry, a[x] is the
// > zero value for the element type of M
memzero(value, uintptr(valueSize))
return false
}
tophash := hashmapTopHash(hash)
bucket := hashmapBucketAddrForHash(m, hash)
// Try to find the key.
for bucket != nil {
for i := uint8(0); i < 8; i++ {
slotKey := hashmapSlotKey(m, bucket, i)
if bucket.tophash[i] == tophash {
// This could be the key we're looking for.
if m.keyEqual(key, hashmapSlotKeyData(m, slotKey), m.keySize) {
// Found the key, copy it.
slotValue := hashmapSlotValue(m, bucket, i)
memcpy(value, hashmapSlotValueData(m, slotValue), m.valueSize)
return true
}
}
}
bucket = bucket.next
}
// Did not find the key.
memzero(value, m.valueSize)
return false
}
// Delete a given key from the map. No-op when the key does not exist in the
// map.
//
//go:nobounds
func hashmapDelete(m *hashmap, key unsafe.Pointer, hash uint32) {
if m == nil {
// The delete builtin is defined even when the map is nil. From the spec:
// > If the map m is nil or the element m[k] does not exist, delete is a
// > no-op.
return
}
tophash := hashmapTopHash(hash)
bucket := hashmapBucketAddrForHash(m, hash)
// Try to find the key.
for bucket != nil {
for i := uint8(0); i < 8; i++ {
slotKey := hashmapSlotKey(m, bucket, i)
if bucket.tophash[i] == tophash {
// This could be the key we're looking for.
if m.keyEqual(key, hashmapSlotKeyData(m, slotKey), m.keySize) {
// Found the key, delete it.
bucket.tophash[i] = 0
// Zero out the slot so the GC won't pin the allocations.
keySlotSize := hashmapKeySlotSize(m)
memzero(slotKey, keySlotSize)
slotValue := hashmapSlotValue(m, bucket, i)
valueSlotSize := hashmapValueSlotSize(m)
memzero(slotValue, valueSlotSize)
m.count--
return
}
}
}
bucket = bucket.next
}
}
// Iterate over a hashmap.
//
//go:nobounds
func hashmapNext(m *hashmap, it *hashmapIterator, key, value unsafe.Pointer) bool {
if m == nil {
// From the spec: If the map is nil, the number of iterations is 0.
return false
}
if it.buckets == nil {
// initialize iterator
it.buckets = m.buckets
it.numBuckets = uintptr(1) << m.bucketBits
it.startBucket = uintptr(fastrand()) & (it.numBuckets - 1)
it.startIndex = uint8(fastrand() & 7)
it.bucketNumber = it.startBucket
it.bucket = hashmapBucketAddr(m, it.buckets, it.bucketNumber)
it.bucketIndex = it.startIndex
}
for {
// If we've wrapped and we're back at our starting location, terminate the iteration.
if it.wrapped && it.bucketNumber == it.startBucket && it.bucketIndex == it.startIndex {
return false
}
if it.bucketIndex >= 8 {
// end of bucket, move to the next in the chain
it.bucketIndex = 0
it.bucket = it.bucket.next
}
if it.bucket == nil {
it.bucketNumber++ // next bucket
if it.bucketNumber >= it.numBuckets {
// went through all buckets -- wrap around
it.bucketNumber = 0
it.wrapped = true
}
it.bucket = hashmapBucketAddr(m, it.buckets, it.bucketNumber)
continue
}
if it.bucket.tophash[it.bucketIndex] == 0 {
// slot is empty - move on
it.bucketIndex++
continue
}
// Found a key.
slotKey := hashmapSlotKey(m, it.bucket, it.bucketIndex)
memcpy(key, hashmapSlotKeyData(m, slotKey), m.keySize)
if it.buckets == m.buckets {
// Our view of the buckets is the same as the parent map.
// Just copy the value we have
slotValue := hashmapSlotValue(m, it.bucket, it.bucketIndex)
memcpy(value, hashmapSlotValueData(m, slotValue), m.valueSize)
it.bucketIndex++
} else {
it.bucketIndex++
// Our view of the buckets doesn't match the parent map.
// Look up the key in the new buckets and return that value if it exists
hash := m.keyHash(key, m.keySize, m.seed)
ok := hashmapGet(m, key, value, m.valueSize, hash)
if !ok {
// doesn't exist in parent map; try next key
continue
}
// All good.
}
return true
}
}
// Hashmap with plain binary data keys (not containing strings etc.).
func hashmapBinarySet(m *hashmap, key, value unsafe.Pointer) {
if m == nil {
nilMapPanic()
}
hash := hash32(key, m.keySize, m.seed)
hashmapSet(m, key, value, hash)
}
func hashmapBinaryGet(m *hashmap, key, value unsafe.Pointer, valueSize uintptr) bool {
if m == nil {
memzero(value, uintptr(valueSize))
return false
}
hash := hash32(key, m.keySize, m.seed)
return hashmapGet(m, key, value, valueSize, hash)
}
func hashmapBinaryDelete(m *hashmap, key unsafe.Pointer) {
if m == nil {
return
}
hash := hash32(key, m.keySize, m.seed)
hashmapDelete(m, key, hash)
}
// Hashmap with compiler-generated key hash/equal functions.
// Unlike the binary path (which uses hash32/memequal), these use the
// type-specific keyHash and keyEqual function pointers stored in the hashmap
// struct. This is used for composite key types (e.g. structs containing
// strings) where the compiler generates specialized hash/equal functions.
func hashmapGenericSet(m *hashmap, key, value unsafe.Pointer) {
if m == nil {
nilMapPanic()
}
hash := m.keyHash(key, m.keySize, m.seed)
hashmapSet(m, key, value, hash)
}
func hashmapGenericGet(m *hashmap, key, value unsafe.Pointer, valueSize uintptr) bool {
if m == nil {
memzero(value, uintptr(valueSize))
return false
}
hash := m.keyHash(key, m.keySize, m.seed)
return hashmapGet(m, key, value, valueSize, hash)
}
func hashmapGenericDelete(m *hashmap, key unsafe.Pointer) {
if m == nil {
return
}
hash := m.keyHash(key, m.keySize, m.seed)
hashmapDelete(m, key, hash)
}
// hashmapMakeGeneric creates a new hashmap with compiler-provided hash and
// equal functions. This avoids the interface/reflection path for composite
// key types like structs containing strings.
func hashmapMakeGeneric(keySize, valueSize uintptr, sizeHint uintptr,
keyHash func(key unsafe.Pointer, size, seed uintptr) uint32,
keyEqual func(x, y unsafe.Pointer, n uintptr) bool) *hashmap {
bucketBits := uint8(0)
for hashmapHasSpaceToGrow(bucketBits) && hashmapOverLoadFactor(sizeHint, bucketBits) {
bucketBits++
}
var flags uint8
keySlotSize := keySize
if keySize > hashmapMaxKeySize {
flags |= hashmapFlagIndirectKey
keySlotSize = unsafe.Sizeof(unsafe.Pointer(nil))
}
valueSlotSize := valueSize
if valueSize > hashmapMaxValueSize {
flags |= hashmapFlagIndirectValue
valueSlotSize = unsafe.Sizeof(unsafe.Pointer(nil))
}
bucketBufSize := hashmapBucketHeaderSize + keySlotSize*8 + valueSlotSize*8
buckets := alloc(bucketBufSize*(1<<bucketBits), nil)
return &hashmap{
buckets: buckets,
seed: uintptr(fastrand()),
keySize: keySize,
valueSize: valueSize,
keySlotSize: keySlotSize,
valueSlotSize: valueSlotSize,
bucketBits: bucketBits,
flags: flags,
keyEqual: keyEqual,
keyHash: keyHash,
}
}
// hashmapMakeReflect creates a hashmap for reflect.MakeMapWithSize using
// closures that reconstruct interface{} values from raw key bytes,
// delegating to hashmapInterfaceHash for hashing and == for equality.
func hashmapMakeReflect(keySize, valueSize, sizeHint uintptr, keyType unsafe.Pointer) *hashmap {
t := (*reflectlite.RawType)(keyType)
if t.Kind() == reflectlite.Interface {
// Interface keys are already stored as interface values in the
// bucket; use the existing interface hash/equal directly.
return hashmapMakeGeneric(keySize, valueSize, sizeHint,
hashmapInterfacePtrHash, hashmapInterfaceEqual)
}
keyHash := func(key unsafe.Pointer, size, seed uintptr) uint32 {
return hashmapInterfaceHash(rawToInterface(t, key), seed)
}
keyEqual := func(x, y unsafe.Pointer, n uintptr) bool {
return rawToInterface(t, x) == rawToInterface(t, y)
}
return hashmapMakeGeneric(keySize, valueSize, sizeHint, keyHash, keyEqual)
}
// rawToInterface reconstructs an interface{} from raw bytes at ptr.
func rawToInterface(t *reflectlite.RawType, ptr unsafe.Pointer) interface{} {
var val unsafe.Pointer
if t.Size() <= unsafe.Sizeof(uintptr(0)) {
val = reflectliteLoadSmallValue(ptr, t.Size())
} else {
val = ptr
}
i := composeInterface(unsafe.Pointer(t), val)
return *(*interface{})(unsafe.Pointer(&i))
}
//go:linkname reflectliteLoadSmallValue internal/reflectlite.loadSmallValue
func reflectliteLoadSmallValue(ptr unsafe.Pointer, size uintptr) unsafe.Pointer
// Hashmap with string keys (a common case).
func hashmapStringEqual(x, y unsafe.Pointer, n uintptr) bool {
return *(*string)(x) == *(*string)(y)
}
func hashmapStringHash(s string, seed uintptr) uint32 {
_s := (*_string)(unsafe.Pointer(&s))
return hash32(unsafe.Pointer(_s.ptr), uintptr(_s.length), seed)
}
func hashmapStringPtrHash(sptr unsafe.Pointer, size uintptr, seed uintptr) uint32 {
_s := *(*_string)(sptr)
return hash32(unsafe.Pointer(_s.ptr), uintptr(_s.length), seed)
}
func hashmapStringSet(m *hashmap, key string, value unsafe.Pointer) {
if m == nil {
nilMapPanic()
}
hash := hashmapStringHash(key, m.seed)
hashmapSet(m, unsafe.Pointer(&key), value, hash)
}
func hashmapStringGet(m *hashmap, key string, value unsafe.Pointer, valueSize uintptr) bool {
if m == nil {
memzero(value, uintptr(valueSize))
return false
}
hash := hashmapStringHash(key, m.seed)
return hashmapGet(m, unsafe.Pointer(&key), value, valueSize, hash)
}
func hashmapStringDelete(m *hashmap, key string) {
if m == nil {
return
}
hash := hashmapStringHash(key, m.seed)
hashmapDelete(m, unsafe.Pointer(&key), hash)
}
// Hashmap with interface keys (for everything else).
// This is a method that is intentionally unexported in the reflect package. It
// is identical to the Interface() method call, except it doesn't check whether
// a field is exported and thus allows circumventing the type system.
// The hash function needs it as it also needs to hash unexported struct fields.
//
//go:linkname valueInterfaceUnsafe internal/reflectlite.valueInterfaceUnsafe
func valueInterfaceUnsafe(v reflectlite.Value) interface{}
func hashmapFloat32Hash(ptr unsafe.Pointer, seed uintptr) uint32 {
f := *(*uint32)(ptr)
if f == 0x80000000 {
// convert -0 to 0 for hashing
f = 0
}
return hash32(unsafe.Pointer(&f), 4, seed)
}
func hashmapFloat64Hash(ptr unsafe.Pointer, seed uintptr) uint32 {
f := *(*uint64)(ptr)
if f == 0x8000000000000000 {
// convert -0 to 0 for hashing
f = 0
}
return hash32(unsafe.Pointer(&f), 8, seed)
}
func hashmapInterfaceHash(itf interface{}, seed uintptr) uint32 {
x := reflectlite.ValueOf(itf)
if x.RawType() == nil {
return 0 // nil interface
}
value := (*_interface)(unsafe.Pointer(&itf)).value
ptr := value
if x.RawType().Size() <= unsafe.Sizeof(uintptr(0)) {
// Value fits in pointer, so it's directly stored in the pointer.
ptr = unsafe.Pointer(&value)
}
switch x.RawType().Kind() {
case reflectlite.Int, reflectlite.Int8, reflectlite.Int16, reflectlite.Int32, reflectlite.Int64:
return hash32(ptr, x.RawType().Size(), seed)
case reflectlite.Bool, reflectlite.Uint, reflectlite.Uint8, reflectlite.Uint16, reflectlite.Uint32, reflectlite.Uint64, reflectlite.Uintptr:
return hash32(ptr, x.RawType().Size(), seed)
case reflectlite.Float32:
// It should be possible to just has the contents. However, NaN != NaN
// so if you're using lots of NaNs as map keys (you shouldn't) then hash
// time may become exponential. To fix that, it would be better to
// return a random number instead:
// https://research.swtch.com/randhash
return hashmapFloat32Hash(ptr, seed)
case reflectlite.Float64:
return hashmapFloat64Hash(ptr, seed)
case reflectlite.Complex64:
rptr, iptr := ptr, unsafe.Add(ptr, 4)
return hashmapFloat32Hash(rptr, seed) ^ hashmapFloat32Hash(iptr, seed)
case reflectlite.Complex128:
rptr, iptr := ptr, unsafe.Add(ptr, 8)
return hashmapFloat64Hash(rptr, seed) ^ hashmapFloat64Hash(iptr, seed)
case reflectlite.String:
return hashmapStringHash(x.String(), seed)
case reflectlite.Chan, reflectlite.Ptr, reflectlite.UnsafePointer:
// It might seem better to just return the pointer, but that won't
// result in an evenly distributed hashmap. Instead, hash the pointer
// like most other types.
return hash32(ptr, x.RawType().Size(), seed)
case reflectlite.Array:
var hash uint32
for i := 0; i < x.Len(); i++ {
hash ^= hashmapInterfaceHash(valueInterfaceUnsafe(x.Index(i)), seed)
}
return hash
case reflectlite.Struct:
var hash uint32
for i := 0; i < x.NumField(); i++ {
hash ^= hashmapInterfaceHash(valueInterfaceUnsafe(x.Field(i)), seed)
}
return hash
default:
runtimePanic("comparing un-comparable type")
return 0 // unreachable
}
}
func hashmapInterfacePtrHash(iptr unsafe.Pointer, size uintptr, seed uintptr) uint32 {
_i := *(*interface{})(iptr)
return hashmapInterfaceHash(_i, seed)
}
func hashmapInterfaceEqual(x, y unsafe.Pointer, n uintptr) bool {
return *(*interface{})(x) == *(*interface{})(y)
}