From 7e05efa274765463859820f8ad3b0bde27927bc9 Mon Sep 17 00:00:00 2001 From: Damian Gryski Date: Mon, 7 Mar 2022 10:52:36 -0800 Subject: [PATCH] src/runtime: first darft of map growth code Fixes #1553 --- compiler/map.go | 14 +++- src/runtime/hashmap.go | 170 ++++++++++++++++++++++++++++++++++------- 2 files changed, 156 insertions(+), 28 deletions(-) diff --git a/compiler/map.go b/compiler/map.go index 62162c0c..57a7033a 100644 --- a/compiler/map.go +++ b/compiler/map.go @@ -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 } diff --git a/src/runtime/hashmap.go b/src/runtime/hashmap.go index c050dae9..c8c12419 100644 --- a/src/runtime/hashmap.go +++ b/src/runtime/hashmap.go @@ -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< 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) }