From 179cf74b0169281855481c0d8f46b438c6883b3c Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Fri, 24 Aug 2018 00:56:20 +0200 Subject: [PATCH] Implement package-global maps (of max 8 entries) --- compiler.go | 98 +++++++++++++++++++++++++++++++++++++ src/examples/hello/hello.go | 4 ++ src/runtime/hashmap.go | 16 ++++-- util.go | 26 ++++++++++ 4 files changed, 139 insertions(+), 5 deletions(-) create mode 100644 util.go diff --git a/compiler.go b/compiler.go index 52d718d4..a29bb900 100644 --- a/compiler.go +++ b/compiler.go @@ -686,6 +686,93 @@ func (c *Compiler) parseInitFunc(frame *Frame) error { // Ignore: CGo pointer conversion. case *ssa.FieldAddr, *ssa.IndexAddr: // Ignore: handled below with *ssa.Store. + case *ssa.MakeMap: + mapType := instr.Type().Underlying().(*types.Map) + llvmKeyType, err := c.getLLVMType(mapType.Key().Underlying()) + if err != nil { + return err + } + llvmValueType, err := c.getLLVMType(mapType.Elem().Underlying()) + if err != nil { + return err + } + // TODO: use the initial map size + bucketType := llvm.StructType([]llvm.Type{ + llvm.ArrayType(llvm.Int8Type(), 8), // tophash + c.i8ptrType, // next bucket + llvm.ArrayType(llvmKeyType, 8), // key type + llvm.ArrayType(llvmValueType, 8), // value type + }, false) + bucket := llvm.AddGlobal(c.mod, bucketType, ".hashmap.bucket") + bucketValue, err := getZeroValue(bucketType) + if err != nil { + return err + } + bucket.SetInitializer(bucketValue) + zero := llvm.ConstInt(llvm.Int32Type(), 0, false) + bucketPtr := llvm.ConstInBoundsGEP(bucket, []llvm.Value{zero}) + keySize := c.targetData.TypeAllocSize(llvmKeyType) + valueSize := c.targetData.TypeAllocSize(llvmValueType) + hashmapType := c.mod.GetTypeByName("runtime.hashmap") + hashmap := llvm.ConstNamedStruct(hashmapType, []llvm.Value{ + llvm.ConstPointerNull(llvm.PointerType(hashmapType, 0)), // next + llvm.ConstBitCast(bucketPtr, c.i8ptrType), // buckets + llvm.ConstInt(c.lenType, 0, false), // count + llvm.ConstInt(llvm.Int8Type(), keySize, false), // keySize + llvm.ConstInt(llvm.Int8Type(), valueSize, false), // valueSize + llvm.ConstInt(llvm.Int8Type(), 0, false), // bucketBits + }) + allocs[instr] = hashmap + case *ssa.MapUpdate: + // Note: we're assuming here the Go SSA compiler knows what it's + // doing and doesn't insert a key twice. + + // Update hashmap.count. + hashmap := allocs[instr.Map] + count := llvm.ConstExtractValue(hashmap, []uint32{2}).ZExtValue() + count++ + countValue := llvm.ConstInt(c.lenType, count + 1, false) + hashmap = llvm.ConstInsertValue(hashmap, countValue, []uint32{2}) + allocs[instr.Map] = hashmap + + // Select the bucket (chain). + bucketPtr := llvm.ConstExtractValue(hashmap, []uint32{1}) + bucketGlobal := bucketPtr.Operand(0) + + llvmKey, err := c.initParseValue(instr.Key, allocs) + if err != nil { + return err + } + llvmValue, err := c.initParseValue(instr.Value, allocs) + if err != nil { + return err + } + + // Hash for the hashtable. This must be equal to what is + // calculated in the runtime implementation. + key := constant.StringVal(instr.Key.(*ssa.Const).Value) + hash := stringhash(&key) + + // Find an empty spot in the bucket. + done := false + for !done { + bucket := bucketGlobal.Initializer() + for i := uint32(0); i < 8; i++ { + if llvm.ConstExtractValue(bucket, []uint32{0, i}).ZExtValue() != 0 { + // already taken + continue + } + tophashValue := llvm.ConstInt(llvm.Int8Type(), uint64(hashmapTopHash(hash)), false) + bucket = llvm.ConstInsertValue(bucket, tophashValue, []uint32{0, i}) + bucket = llvm.ConstInsertValue(bucket, llvmKey, []uint32{2, i}) + bucket = llvm.ConstInsertValue(bucket, llvmValue, []uint32{3, i}) + bucketGlobal.SetInitializer(bucket) + done = true + } + if !done { + return errors.New("init: todo: allocate a new bucket") + } + } case *ssa.Slice: // Turn a just-allocated array into a slice. if instr.Low != nil || instr.High != nil || instr.Max != nil { @@ -735,6 +822,17 @@ func (c *Compiler) parseInitFunc(frame *Frame) error { if err != nil { return err } + if _, ok := instr.Val.Type().Underlying().(*types.Map); ok { + // Store pointer to map instead of the map itself. It is + // a reference type, so every access goes through a + // pointer to the real value. Note that this has to be a + // pointer in some cases as the global value can be + // replaced with a different map. + hashmap := llvm.AddGlobal(c.mod, val.Type(), ".hashmap") + hashmap.SetInitializer(val) + zero := llvm.ConstInt(llvm.Int32Type(), 0, false) + val = llvm.ConstInBoundsGEP(hashmap, []llvm.Value{zero}) + } llvmAddr := c.ir.GetGlobal(addr).llvmGlobal llvmAddr.SetInitializer(val) default: diff --git a/src/examples/hello/hello.go b/src/examples/hello/hello.go index bc85296d..489aff22 100644 --- a/src/examples/hello/hello.go +++ b/src/examples/hello/hello.go @@ -14,6 +14,8 @@ type Stringer interface { const SIX = 6 +var testmap = map[string]int{"data": 3} + func main() { println("Hello world from Go!") println("The answer is:", calculateAnswer()) @@ -25,6 +27,7 @@ func main() { m := map[string]int{"answer": 42, "foo": 3} readMap(m, "answer") + readMap(testmap, "data") foo := []int{1, 2, 4, 5} println("len/cap foo:", len(foo), cap(foo)) @@ -50,6 +53,7 @@ func runFunc(f func(int), arg int) { } func readMap(m map[string]int, key string) { + println("map length:", len(m)) println("map read:", key, "=", m[key]) } diff --git a/src/runtime/hashmap.go b/src/runtime/hashmap.go index 33dcbdb2..3ab97cd3 100644 --- a/src/runtime/hashmap.go +++ b/src/runtime/hashmap.go @@ -42,6 +42,16 @@ func stringhash(s *string) uint32 { return result } +// 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 uint8) *hashmap { bucketBufSize := unsafe.Sizeof(hashmapBucket{}) + uintptr(keySize)*8 + uintptr(valueSize)*8 @@ -63,11 +73,7 @@ func hashmapSet(m *hashmap, key string, value unsafe.Pointer) { bucketAddr := uintptr(m.buckets) + bucketSize*bucketNumber bucket := (*hashmapBucket)(unsafe.Pointer(bucketAddr)) - tophash := uint8(hash >> 24) - if tophash < 1 { - // 0 means empty slot, so make it bigger. - tophash += 1 - } + tophash := hashmapTopHash(hash) // See whether the key already exists somewhere. var emptySlotKey *string diff --git a/util.go b/util.go new file mode 100644 index 00000000..800b463d --- /dev/null +++ b/util.go @@ -0,0 +1,26 @@ +package main + +// This file contains functions that are also used by the runtime. These must be +// kept in sync. + +// Get FNV-1a hash of this string. +// +// https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function#FNV-1a_hash +func stringhash(s *string) uint32 { + var result uint32 = 2166136261 // FNV offset basis + for i := 0; i < len(*s); i++ { + result ^= uint32((*s)[i]) + result *= 16777619 // FNV prime + } + return result +} + +// 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 +}