Skip to content

Commit

Permalink
optimization: improve scanner.Map's performance by caching resolved…
Browse files Browse the repository at this point in the history
… tags
  • Loading branch information
fangwentong committed May 17, 2022
1 parent 67b08c6 commit f67276e
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 4 deletions.
9 changes: 6 additions & 3 deletions scanner/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,17 @@ ScanMapClose is the same as ScanMap but it also close the rows
Test cases blow may make sense

```go
package scaner
package scaner_test

import (
"github.com/didi/gendry/scanner"
"github.com/stretchr/testify/assert"
"testing"
)

func TestMap(t *testing.T) {
// you can improve the performance of `scanner.Map` by scanner.EnableMapNameCache(true)
scanner.EnableMapNameCache(true)
type Person struct {
Name string `ddb:"name"`
Age int `ddb:"age"`
Expand All @@ -65,14 +68,14 @@ func TestMap(t *testing.T) {
a := Person{"deen", 22, 1}
b := &Person{"caibirdme", 23, 1}
c := &b
mapA, err := Map(a, DefaultTagName)
mapA, err := scanner.Map(a, scanner.DefaultTagName)
ass := assert.New(t)
ass.NoError(err)
ass.Equal("deen", mapA["name"])
ass.Equal(22, mapA["age"])
_, ok := mapA["foo"]
ass.False(ok)
mapB, err := Map(c, "")
mapB, err := scanner.Map(c, "")
ass.NoError(err)
ass.Equal("caibirdme", mapB["Name"])
ass.Equal(23, mapB["Age"])
Expand Down
67 changes: 66 additions & 1 deletion scanner/map.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,25 @@ import (
"errors"
"reflect"
"strings"
"sync"
)

var (
// ErrNoneStructTarget as its name says
ErrNoneStructTarget = errors.New("[scanner] target must be a struct type")

enableMapNameCache bool

nameCache = &keyNameCache{
nameMap: make(map[reflect.Type]map[string][]string),
}
)

// EnableMapNameCache improve performance by caching resolved key names
func EnableMapNameCache(isEnable bool) {
enableMapNameCache = isEnable
}

// Map converts a struct to a map
// type for each field of the struct must be built-in type
func Map(target interface{}, useTag string) (map[string]interface{}, error) {
Expand All @@ -26,8 +38,21 @@ func Map(target interface{}, useTag string) (map[string]interface{}, error) {
}
t := v.Type()
result := make(map[string]interface{})

var getKeyName func(int) string
if enableMapNameCache {
names := nameCache.GetKeyNames(t, useTag)
getKeyName = func(i int) string {
return names[i]
}
} else {
getKeyName = func(i int) string {
return getKey(t.Field(i), useTag)
}
}

for i := 0; i < t.NumField(); i++ {
keyName := getKey(t.Field(i), useTag)
keyName := getKeyName(i)
if "" == keyName {
continue
}
Expand Down Expand Up @@ -64,3 +89,43 @@ func resolveTagName(tag string) string {
}
return tag[:idx]
}

type keyNameCache struct {
mutex sync.Mutex

// nameMap: type of struct -> map(tag -> resolved key names of the struct, array indexed by field index)
nameMap map[reflect.Type]map[string][]string
}

func (cache *keyNameCache) GetKeyNames(t reflect.Type, useTag string) []string {
names := cache.nameMap[t][useTag]
if names != nil {
return names
}
cache.mutex.Lock()
defer cache.mutex.Unlock()
// double check
names = cache.nameMap[t][useTag]
if names != nil {
return names
}
// resolve names then set to cache
names = cache.resolveKeyNamesOf(t, useTag)
if m, ok := cache.nameMap[t]; ok {
m[useTag] = names
} else {
m = make(map[string][]string)
cache.nameMap[t] = m
m[useTag] = names
}
return names
}

func (cache *keyNameCache) resolveKeyNamesOf(t reflect.Type, useTag string) []string {
result := make([]string, t.NumField())
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
result[i] = getKey(field, useTag)
}
return result
}
27 changes: 27 additions & 0 deletions scanner/map_bench_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package scanner

import (
"testing"
)

var p = 12
var a = person{"deen", 22, 1, &p}

// go test -run=BenchmarkMap -bench=BenchmarkMap -cpu=1,2,4,8 -benchtime=20000000x -benchmem
func BenchmarkMapWithCache(b *testing.B) {
EnableMapNameCache(true)
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_, _ = Map(&a, DefaultTagName)
}
})
}

func BenchmarkMapDisableCache(b *testing.B) {
EnableMapNameCache(false)
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_, _ = Map(&a, DefaultTagName)
}
})
}

0 comments on commit f67276e

Please sign in to comment.