Skip to content

Commit

Permalink
use cow map intintmap to cache type information goccy#194
Browse files Browse the repository at this point in the history
Change-Id: Icb767d22b6c1b2cad991c53f3f450f5eb3f773b1
  • Loading branch information
jxskiss committed May 2, 2021
1 parent 4d51b8b commit 6464ba0
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 47 deletions.
24 changes: 3 additions & 21 deletions codec.go
Expand Up @@ -3,7 +3,6 @@ package json
import (
"fmt"
"reflect"
"sync/atomic"
"unsafe"
)

Expand All @@ -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
Expand Down Expand Up @@ -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)))
}
9 changes: 5 additions & 4 deletions decode_compile.go
Expand Up @@ -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
}

Expand Down
27 changes: 5 additions & 22 deletions internal/encoder/compiler.go
Expand Up @@ -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"
)

Expand All @@ -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
)

Expand All @@ -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 )
Expand All @@ -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
}

Expand Down
156 changes: 156 additions & 0 deletions 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
}
28 changes: 28 additions & 0 deletions 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)
}
}

0 comments on commit 6464ba0

Please sign in to comment.