From 6464ba067325e9558e2f7521d72ca8d59d93c9f0 Mon Sep 17 00:00:00 2001 From: Shawn Wang Date: Sun, 2 May 2021 22:58:45 +0800 Subject: [PATCH] use cow map intintmap to cache type information #194 Change-Id: Icb767d22b6c1b2cad991c53f3f450f5eb3f773b1 --- codec.go | 24 +---- decode_compile.go | 9 +- internal/encoder/compiler.go | 27 +----- internal/iimap/intintmap.go | 156 +++++++++++++++++++++++++++++++ internal/iimap/intintmap_test.go | 28 ++++++ 5 files changed, 197 insertions(+), 47 deletions(-) create mode 100644 internal/iimap/intintmap.go create mode 100644 internal/iimap/intintmap_test.go diff --git a/codec.go b/codec.go index 8818bb53..9a88258c 100644 --- a/codec.go +++ b/codec.go @@ -3,7 +3,6 @@ package json import ( "fmt" "reflect" - "sync/atomic" "unsafe" ) @@ -12,10 +11,9 @@ const ( ) var ( - cachedDecoder []decoder - cachedDecoderMap unsafe.Pointer // map[uintptr]decoder - baseTypeAddr uintptr - maxTypeAddr uintptr + cachedDecoder []decoder + baseTypeAddr uintptr + maxTypeAddr uintptr ) //go:linkname typelinks reflect.typelinks @@ -73,19 +71,3 @@ func setupCodec() error { func init() { _ = setupCodec() } - -func loadDecoderMap() map[uintptr]decoder { - p := atomic.LoadPointer(&cachedDecoderMap) - return *(*map[uintptr]decoder)(unsafe.Pointer(&p)) -} - -func storeDecoder(typ uintptr, dec decoder, m map[uintptr]decoder) { - newDecoderMap := make(map[uintptr]decoder, len(m)+1) - newDecoderMap[typ] = dec - - for k, v := range m { - newDecoderMap[k] = v - } - - atomic.StorePointer(&cachedDecoderMap, *(*unsafe.Pointer)(unsafe.Pointer(&newDecoderMap))) -} diff --git a/decode_compile.go b/decode_compile.go index 62f3a610..3dbeee4e 100644 --- a/decode_compile.go +++ b/decode_compile.go @@ -8,24 +8,25 @@ import ( "unicode" "unsafe" + "github.com/goccy/go-json/internal/iimap" "github.com/goccy/go-json/internal/runtime" ) var ( jsonNumberType = reflect.TypeOf(json.Number("")) + decoderMap = iimap.NewTypeMap() ) func decodeCompileToGetDecoderSlowPath(typeptr uintptr, typ *rtype) (decoder, error) { - decoderMap := loadDecoderMap() - if dec, exists := decoderMap[typeptr]; exists { - return dec, nil + if dec := decoderMap.Get(typeptr); dec != nil { + return dec.(decoder), nil } dec, err := decodeCompileHead(typ, map[uintptr]decoder{}) if err != nil { return nil, err } - storeDecoder(typeptr, dec, decoderMap) + decoderMap.Set(typeptr, dec) return dec, nil } diff --git a/internal/encoder/compiler.go b/internal/encoder/compiler.go index e55d7f61..b9fbf5c0 100644 --- a/internal/encoder/compiler.go +++ b/internal/encoder/compiler.go @@ -6,10 +6,10 @@ import ( "fmt" "reflect" "strings" - "sync/atomic" "unsafe" "github.com/goccy/go-json/internal/errors" + "github.com/goccy/go-json/internal/iimap" "github.com/goccy/go-json/internal/runtime" ) @@ -18,7 +18,7 @@ var ( marshalTextType = reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem() jsonNumberType = reflect.TypeOf(json.Number("")) cachedOpcodeSets []*OpcodeSet - cachedOpcodeMap unsafe.Pointer // map[uintptr]*OpcodeSet + cachedOpcodeMap = iimap.NewTypeMap() typeAddr *runtime.TypeAddr ) @@ -30,26 +30,9 @@ func init() { cachedOpcodeSets = make([]*OpcodeSet, typeAddr.AddrRange) } -func loadOpcodeMap() map[uintptr]*OpcodeSet { - p := atomic.LoadPointer(&cachedOpcodeMap) - return *(*map[uintptr]*OpcodeSet)(unsafe.Pointer(&p)) -} - -func storeOpcodeSet(typ uintptr, set *OpcodeSet, m map[uintptr]*OpcodeSet) { - newOpcodeMap := make(map[uintptr]*OpcodeSet, len(m)+1) - newOpcodeMap[typ] = set - - for k, v := range m { - newOpcodeMap[k] = v - } - - atomic.StorePointer(&cachedOpcodeMap, *(*unsafe.Pointer)(unsafe.Pointer(&newOpcodeMap))) -} - func compileToGetCodeSetSlowPath(typeptr uintptr) (*OpcodeSet, error) { - opcodeMap := loadOpcodeMap() - if codeSet, exists := opcodeMap[typeptr]; exists { - return codeSet, nil + if codeSet := cachedOpcodeMap.Get(typeptr); codeSet != nil { + return codeSet.(*OpcodeSet), nil } // noescape trick for header.typ ( reflect.*rtype ) @@ -69,7 +52,7 @@ func compileToGetCodeSetSlowPath(typeptr uintptr) (*OpcodeSet, error) { Code: code, CodeLength: codeLength, } - storeOpcodeSet(typeptr, codeSet, opcodeMap) + cachedOpcodeMap.Set(typeptr, codeSet) return codeSet, nil } diff --git a/internal/iimap/intintmap.go b/internal/iimap/intintmap.go new file mode 100644 index 00000000..ecb82bf8 --- /dev/null +++ b/internal/iimap/intintmap.go @@ -0,0 +1,156 @@ +package iimap + +import ( + "math" + "sync/atomic" + "unsafe" +) + +// TypeMap is a lockless copy-on-write map to use for type information cache. +// The fill factor used the TypeMap is 0.6. +// A TypeMap will grow as needed. +type TypeMap struct { + m unsafe.Pointer // *iiMap +} + +// NewTypeMap returns a new TypeMap with 8 as initial capacity. +func NewTypeMap() *TypeMap { + capacity := 8 + iim := newIIMap(capacity) + return &TypeMap{m: unsafe.Pointer(iim)} +} + +func (m *TypeMap) Size() int { + return (*iiMap)(atomic.LoadPointer(&m.m)).size +} + +// Get returns the value if the key is found in the map. +func (m *TypeMap) Get(key uintptr) interface{} { + return (*iiMap)(atomic.LoadPointer(&m.m)).Get(key) +} + +// Set adds or updates key with value to the map, if the key value +// is not present in the underlying map, it will copy the map and +// add the key value to the copy, then swap to the new map using atomic +// operation. +func (m *TypeMap) Set(key uintptr, val interface{}) { + mm := (*iiMap)(atomic.LoadPointer(&m.m)) + if v := mm.Get(key); v == val { + return + } + + newMap := mm.Copy() + newMap.Set(key, val) + atomic.StorePointer(&m.m, unsafe.Pointer(newMap)) +} + +// -------- int interface map -------- // + +const ( + intPhi = 0x9E3779B9 + freeKey = 0 + fillFactor = 0.6 + + ptrsize = unsafe.Sizeof(uintptr(0)) +) + +func phiMix(x uintptr) uint64 { + h := uint64(x * intPhi) + return h ^ (h >> 16) +} + +func calcThreshold(capacity int) int { + return int(math.Floor(float64(capacity) * fillFactor)) +} + +type iiMap struct { + data []iiEntry + dataptr unsafe.Pointer + + threshold int + size int + mask uint64 +} + +type iiEntry struct { + K uintptr + V interface{} +} + +func newIIMap(capacity int) *iiMap { + if capacity&(capacity-1) != 0 { + panic("capacity must be power of two") + } + threshold := calcThreshold(capacity) + mask := capacity - 1 + data := make([]iiEntry, capacity) + return &iiMap{ + data: data, + dataptr: unsafe.Pointer(&data[0]), + threshold: threshold, + size: 0, + mask: uint64(mask), + } +} + +// getK helps to eliminate slice bounds checking +func (m *iiMap) getK(ptr uint64) *uintptr { + return (*uintptr)(unsafe.Pointer(uintptr(m.dataptr) + uintptr(ptr)*3*ptrsize)) +} + +// getV helps to eliminate slice bounds checking +func (m *iiMap) getV(ptr uint64) *interface{} { + return (*interface{})(unsafe.Pointer(uintptr(m.dataptr) + uintptr(ptr)*3*ptrsize + ptrsize)) +} + +func (m *iiMap) Get(key uintptr) interface{} { + // manually inline phiMix to help inlining + h := uint64(key * intPhi) + ptr := h ^ (h >> 16) + + for { + ptr &= m.mask + k := *m.getK(ptr) + if k == key { + return *m.getV(ptr) + } + if k == freeKey { + return nil + } + ptr += 1 + } +} + +func (m *iiMap) Set(key uintptr, val interface{}) { + ptr := phiMix(key) + for { + ptr &= m.mask + k := *m.getK(ptr) + if k == freeKey { + *m.getK(ptr) = key + *m.getV(ptr) = val + m.size++ + return + } + if k == key { + *m.getV(ptr) = val + return + } + ptr += 1 + } +} + +func (m *iiMap) Copy() *iiMap { + capacity := cap(m.data) + if m.size >= m.threshold { + capacity *= 2 + } + newMap := newIIMap(capacity) + for _, e := range m.data { + if e.K == freeKey { + continue + } + newMap.Set(e.K, e.V) + } + return newMap +} diff --git a/internal/iimap/intintmap_test.go b/internal/iimap/intintmap_test.go new file mode 100644 index 00000000..340f87ae --- /dev/null +++ b/internal/iimap/intintmap_test.go @@ -0,0 +1,28 @@ +package iimap + +import "testing" + +func TestTypeMapSimple(t *testing.T) { + m := NewTypeMap() + var i uintptr + var v interface{} + + const n = 20000 + + for i = 2; i < n; i += 2 { + m.Set(i, i) + } + + for i = 2; i < n; i += 2 { + if v = m.Get(i); v != i { + t.Errorf("didn't get expected value, %v, %v", i, v) + } + if v = m.Get(i + 1); v != nil { + t.Errorf("didn't get expected 'not found' flag") + } + } + + if m.Size() != (n-2)/2 { + t.Errorf("size (%d) is not right, should be %d", m.Size(), n-1) + } +}