src/runtime: first darft of map growth code

Fixes #1553
Этот коммит содержится в:
Damian Gryski 2022-03-07 10:52:36 -08:00 коммит произвёл Ron Evans
родитель b02cea0321
коммит 7e05efa274
2 изменённых файлов: 156 добавлений и 28 удалений

Просмотреть файл

@ -10,6 +10,13 @@ import (
"tinygo.org/x/go-llvm"
)
// constants for hashmap algorithms; must match src/runtime/hashmap.go
const (
hashmapAlgorithmBinary = iota
hashmapAlgorithmString
hashmapAlgorithmInterface
)
// createMakeMap creates a new map object (runtime.hashmap) by allocating and
// initializing an appropriately sized object.
func (b *builder) createMakeMap(expr *ssa.MakeMap) (llvm.Value, error) {
@ -17,22 +24,27 @@ func (b *builder) createMakeMap(expr *ssa.MakeMap) (llvm.Value, error) {
keyType := mapType.Key().Underlying()
llvmValueType := b.getLLVMType(mapType.Elem().Underlying())
var llvmKeyType llvm.Type
var alg uint64 // must match values in src/runtime/hashmap.go
if t, ok := keyType.(*types.Basic); ok && t.Info()&types.IsString != 0 {
// String keys.
llvmKeyType = b.getLLVMType(keyType)
alg = hashmapAlgorithmString
} else if hashmapIsBinaryKey(keyType) {
// Trivially comparable keys.
llvmKeyType = b.getLLVMType(keyType)
alg = hashmapAlgorithmBinary
} else {
// All other keys. Implemented as map[interface{}]valueType for ease of
// implementation.
llvmKeyType = b.getLLVMRuntimeType("_interface")
alg = hashmapAlgorithmInterface
}
keySize := b.targetData.TypeAllocSize(llvmKeyType)
valueSize := b.targetData.TypeAllocSize(llvmValueType)
llvmKeySize := llvm.ConstInt(b.ctx.Int8Type(), keySize, false)
llvmValueSize := llvm.ConstInt(b.ctx.Int8Type(), valueSize, false)
sizeHint := llvm.ConstInt(b.uintptrType, 8, false)
algEnum := llvm.ConstInt(b.ctx.Int8Type(), alg, false)
if expr.Reserve != nil {
sizeHint = b.getValue(expr.Reserve)
var err error
@ -41,7 +53,7 @@ func (b *builder) createMakeMap(expr *ssa.MakeMap) (llvm.Value, error) {
return llvm.Value{}, err
}
}
hashmap := b.createRuntimeCall("hashmapMake", []llvm.Value{llvmKeySize, llvmValueSize, sizeHint}, "")
hashmap := b.createRuntimeCall("hashmapMake", []llvm.Value{llvmKeySize, llvmValueSize, sizeHint, algEnum}, "")
return hashmap, nil
}

Просмотреть файл

@ -12,14 +12,23 @@ import (
// The underlying hashmap structure for Go.
type hashmap struct {
next *hashmap // hashmap after evacuate (for iterators)
buckets unsafe.Pointer // pointer to array of buckets
count uintptr
keySize uint8 // maybe this can store the key type as well? E.g. keysize == 5 means string?
valueSize uint8
bucketBits uint8
keyEqual func(x, y unsafe.Pointer, n uintptr) bool
keyHash func(key unsafe.Pointer, size uintptr) uint32
}
type hashmapAlgorithm uint8
const (
hashmapAlgorithmBinary hashmapAlgorithm = iota
hashmapAlgorithmString
hashmapAlgorithmInterface
)
// 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
@ -32,6 +41,8 @@ type hashmapBucket struct {
}
type hashmapIterator struct {
buckets unsafe.Pointer
numBuckets uintptr
bucketNumber uintptr
bucket *hashmapBucket
bucketIndex uint8
@ -48,7 +59,7 @@ func hashmapTopHash(hash uint32) uint8 {
}
// Create a new hashmap with the given keySize and valueSize.
func hashmapMake(keySize, valueSize uint8, sizeHint uintptr) *hashmap {
func hashmapMake(keySize, valueSize uint8, sizeHint uintptr, alg uint8) *hashmap {
numBuckets := sizeHint / 8
bucketBits := uint8(0)
for numBuckets != 0 {
@ -57,14 +68,58 @@ func hashmapMake(keySize, valueSize uint8, sizeHint uintptr) *hashmap {
}
bucketBufSize := unsafe.Sizeof(hashmapBucket{}) + uintptr(keySize)*8 + uintptr(valueSize)*8
buckets := alloc(bucketBufSize*(1<<bucketBits), nil)
keyHash := hashmapKeyHashAlg(hashmapAlgorithm(alg))
keyEqual := hashmapKeyEqualAlg(hashmapAlgorithm(alg))
return &hashmap{
buckets: buckets,
keySize: keySize,
valueSize: valueSize,
bucketBits: bucketBits,
keyEqual: keyEqual,
keyHash: keyHash,
}
}
func hashmapKeyEqualAlg(alg hashmapAlgorithm) func(x, y unsafe.Pointer, n uintptr) bool {
switch alg {
case hashmapAlgorithmBinary:
return memequal
case hashmapAlgorithmString:
return hashmapStringEqual
case hashmapAlgorithmInterface:
return hashmapInterfaceEqual
default:
panic("unknown hashmap equal algorithm")
}
}
func hashmapKeyHashAlg(alg hashmapAlgorithm) func(key unsafe.Pointer, n uintptr) uint32 {
switch alg {
case hashmapAlgorithmBinary:
return hash32
case hashmapAlgorithmString:
return hashmapStringPtrHash
case hashmapAlgorithmInterface:
return hashmapInterfacePtrHash
default:
panic("unknown hashmap hash algorithm")
}
}
func hashmapShouldGrow(m *hashmap) bool {
if m.bucketBits == 32 {
// can't grow any more
return false
}
// "maximum" number of elements is 0.75 * buckets * elements per bucket
max := uintptr((3 * ((1 << m.bucketBits) * 8)) / 4)
return m.count > 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
@ -83,7 +138,7 @@ func hashmapLenUnsafePointer(p unsafe.Pointer) int {
// 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, keyEqual func(x, y unsafe.Pointer, n uintptr) bool) {
func hashmapSet(m *hashmap, key unsafe.Pointer, value unsafe.Pointer, hash uint32) {
tophash := hashmapTopHash(hash)
if m.buckets == nil {
@ -92,6 +147,10 @@ func hashmapSet(m *hashmap, key unsafe.Pointer, value unsafe.Pointer, hash uint3
return
}
if hashmapShouldGrow(m) {
hashmapGrow(m)
}
numBuckets := uintptr(1) << m.bucketBits
bucketNumber := (uintptr(hash) & (numBuckets - 1))
bucketSize := unsafe.Sizeof(hashmapBucket{}) + uintptr(m.keySize)*8 + uintptr(m.valueSize)*8
@ -118,7 +177,7 @@ func hashmapSet(m *hashmap, key unsafe.Pointer, value unsafe.Pointer, hash uint3
}
if bucket.tophash[i] == tophash {
// Could be an existing key that's the same.
if keyEqual(key, slotKey, uintptr(m.keySize)) {
if m.keyEqual(key, slotKey, uintptr(m.keySize)) {
// found same key, replace it
memcpy(slotValue, value, uintptr(m.valueSize))
return
@ -158,9 +217,35 @@ func hashmapInsertIntoNewBucket(m *hashmap, key, value unsafe.Pointer, tophash u
return bucket
}
func hashmapGrow(m *hashmap) {
// clone map as empty
n := *m
n.count = 0
// allocate our new buckets twice as big
n.bucketBits = m.bucketBits + 1
numBuckets := uintptr(1) << n.bucketBits
bucketBufSize := unsafe.Sizeof(hashmapBucket{}) + uintptr(m.keySize)*8 + uintptr(m.valueSize)*8
n.buckets = alloc(bucketBufSize*numBuckets, nil)
// use a hashmap iterator to go through the old map
var it hashmapIterator
var key = alloc(uintptr(m.keySize), nil)
var value = alloc(uintptr(m.valueSize), nil)
for hashmapNext(m, &it, key, value) {
h := m.keyHash(key, uintptr(m.keySize))
hashmapSet(&n, key, value, h)
}
*m = 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, keyEqual func(x, y unsafe.Pointer, n uintptr) bool) bool {
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
@ -189,7 +274,7 @@ func hashmapGet(m *hashmap, key, value unsafe.Pointer, valueSize uintptr, hash u
slotValue := unsafe.Pointer(uintptr(unsafe.Pointer(bucket)) + slotValueOffset)
if bucket.tophash[i] == tophash {
// This could be the key we're looking for.
if keyEqual(key, slotKey, uintptr(m.keySize)) {
if m.keyEqual(key, slotKey, uintptr(m.keySize)) {
// Found the key, copy it.
memcpy(value, slotValue, uintptr(m.valueSize))
return true
@ -207,7 +292,7 @@ func hashmapGet(m *hashmap, key, value unsafe.Pointer, valueSize uintptr, hash u
// 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, keyEqual func(x, y unsafe.Pointer, n uintptr) bool) {
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
@ -233,7 +318,7 @@ func hashmapDelete(m *hashmap, key unsafe.Pointer, hash uint32, keyEqual func(x,
slotKey := unsafe.Pointer(uintptr(unsafe.Pointer(bucket)) + slotKeyOffset)
if bucket.tophash[i] == tophash {
// This could be the key we're looking for.
if keyEqual(key, slotKey, uintptr(m.keySize)) {
if m.keyEqual(key, slotKey, uintptr(m.keySize)) {
// Found the key, delete it.
bucket.tophash[i] = 0
m.count--
@ -249,13 +334,16 @@ func hashmapDelete(m *hashmap, key unsafe.Pointer, hash uint32, keyEqual func(x,
//go:nobounds
func hashmapNext(m *hashmap, it *hashmapIterator, key, value unsafe.Pointer) bool {
if m == nil {
// Iterating over a nil slice appears to be allowed by the Go spec:
// https://groups.google.com/g/golang-nuts/c/gVgVLQU1FFE?pli=1
// https://play.golang.org/p/S8jxAMytKDB
// From the spec: If the map is nil, the number of iterations is 0.
return false
}
numBuckets := uintptr(1) << m.bucketBits
if it.buckets == nil {
// initialize iterator
it.buckets = m.buckets
it.numBuckets = uintptr(1) << m.bucketBits
}
for {
if it.bucketIndex >= 8 {
// end of bucket, move to the next in the chain
@ -263,12 +351,12 @@ func hashmapNext(m *hashmap, it *hashmapIterator, key, value unsafe.Pointer) boo
it.bucket = it.bucket.next
}
if it.bucket == nil {
if it.bucketNumber >= numBuckets {
if it.bucketNumber >= it.numBuckets {
// went through all buckets
return false
}
bucketSize := unsafe.Sizeof(hashmapBucket{}) + uintptr(m.keySize)*8 + uintptr(m.valueSize)*8
bucketAddr := uintptr(m.buckets) + bucketSize*it.bucketNumber
bucketAddr := uintptr(it.buckets) + bucketSize*it.bucketNumber
it.bucket = (*hashmapBucket)(unsafe.Pointer(bucketAddr))
it.bucketNumber++ // next bucket
}
@ -281,11 +369,29 @@ func hashmapNext(m *hashmap, it *hashmapIterator, key, value unsafe.Pointer) boo
bucketAddr := uintptr(unsafe.Pointer(it.bucket))
slotKeyOffset := unsafe.Sizeof(hashmapBucket{}) + uintptr(m.keySize)*uintptr(it.bucketIndex)
slotKey := unsafe.Pointer(bucketAddr + slotKeyOffset)
slotValueOffset := unsafe.Sizeof(hashmapBucket{}) + uintptr(m.keySize)*8 + uintptr(m.valueSize)*uintptr(it.bucketIndex)
slotValue := unsafe.Pointer(bucketAddr + slotValueOffset)
memcpy(key, slotKey, uintptr(m.keySize))
memcpy(value, slotValue, uintptr(m.valueSize))
it.bucketIndex++
if it.buckets == m.buckets {
// Our view of the buckets is the same as the parent map.
// Just copy the value we have
slotValueOffset := unsafe.Sizeof(hashmapBucket{}) + uintptr(m.keySize)*8 + uintptr(m.valueSize)*uintptr(it.bucketIndex)
slotValue := unsafe.Pointer(bucketAddr + slotValueOffset)
memcpy(value, slotValue, uintptr(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, uintptr(m.keySize))
ok := hashmapGet(m, key, value, uintptr(m.valueSize), hash)
if !ok {
// doesn't exist in parent map; try next key
continue
}
// All good.
}
return true
}
@ -296,7 +402,7 @@ func hashmapNext(m *hashmap, it *hashmapIterator, key, value unsafe.Pointer) boo
func hashmapBinarySet(m *hashmap, key, value unsafe.Pointer) {
// TODO: detect nil map here and throw a better panic message?
hash := hash32(key, uintptr(m.keySize))
hashmapSet(m, key, value, hash, memequal)
hashmapSet(m, key, value, hash)
}
func hashmapBinaryGet(m *hashmap, key, value unsafe.Pointer, valueSize uintptr) bool {
@ -305,7 +411,7 @@ func hashmapBinaryGet(m *hashmap, key, value unsafe.Pointer, valueSize uintptr)
return false
}
hash := hash32(key, uintptr(m.keySize))
return hashmapGet(m, key, value, valueSize, hash, memequal)
return hashmapGet(m, key, value, valueSize, hash)
}
func hashmapBinaryDelete(m *hashmap, key unsafe.Pointer) {
@ -313,7 +419,7 @@ func hashmapBinaryDelete(m *hashmap, key unsafe.Pointer) {
return
}
hash := hash32(key, uintptr(m.keySize))
hashmapDelete(m, key, hash, memequal)
hashmapDelete(m, key, hash)
}
// Hashmap with string keys (a common case).
@ -327,19 +433,24 @@ func hashmapStringHash(s string) uint32 {
return hash32(unsafe.Pointer(_s.ptr), uintptr(_s.length))
}
func hashmapStringPtrHash(sptr unsafe.Pointer, size uintptr) uint32 {
_s := *(*_string)(sptr)
return hash32(unsafe.Pointer(_s.ptr), uintptr(_s.length))
}
func hashmapStringSet(m *hashmap, key string, value unsafe.Pointer) {
hash := hashmapStringHash(key)
hashmapSet(m, unsafe.Pointer(&key), value, hash, hashmapStringEqual)
hashmapSet(m, unsafe.Pointer(&key), value, hash)
}
func hashmapStringGet(m *hashmap, key string, value unsafe.Pointer, valueSize uintptr) bool {
hash := hashmapStringHash(key)
return hashmapGet(m, unsafe.Pointer(&key), value, valueSize, hash, hashmapStringEqual)
return hashmapGet(m, unsafe.Pointer(&key), value, valueSize, hash)
}
func hashmapStringDelete(m *hashmap, key string) {
hash := hashmapStringHash(key)
hashmapDelete(m, unsafe.Pointer(&key), hash, hashmapStringEqual)
hashmapDelete(m, unsafe.Pointer(&key), hash)
}
// Hashmap with interface keys (for everything else).
@ -427,21 +538,26 @@ func hashmapInterfaceHash(itf interface{}) uint32 {
}
}
func hashmapInterfacePtrHash(iptr unsafe.Pointer, size uintptr) uint32 {
_i := *(*_interface)(iptr)
return hashmapInterfaceHash(_i)
}
func hashmapInterfaceEqual(x, y unsafe.Pointer, n uintptr) bool {
return *(*interface{})(x) == *(*interface{})(y)
}
func hashmapInterfaceSet(m *hashmap, key interface{}, value unsafe.Pointer) {
hash := hashmapInterfaceHash(key)
hashmapSet(m, unsafe.Pointer(&key), value, hash, hashmapInterfaceEqual)
hashmapSet(m, unsafe.Pointer(&key), value, hash)
}
func hashmapInterfaceGet(m *hashmap, key interface{}, value unsafe.Pointer, valueSize uintptr) bool {
hash := hashmapInterfaceHash(key)
return hashmapGet(m, unsafe.Pointer(&key), value, valueSize, hash, hashmapInterfaceEqual)
return hashmapGet(m, unsafe.Pointer(&key), value, valueSize, hash)
}
func hashmapInterfaceDelete(m *hashmap, key interface{}) {
hash := hashmapInterfaceHash(key)
hashmapDelete(m, unsafe.Pointer(&key), hash, hashmapInterfaceEqual)
hashmapDelete(m, unsafe.Pointer(&key), hash)
}