From f67276edf8fd856df14c6acd22fc392d137b8e83 Mon Sep 17 00:00:00 2001 From: fangwentong Date: Wed, 18 May 2022 00:54:07 +0800 Subject: [PATCH] optimization: improve `scanner.Map`'s performance by caching resolved tags --- scanner/README.md | 9 ++++-- scanner/map.go | 67 ++++++++++++++++++++++++++++++++++++++- scanner/map_bench_test.go | 27 ++++++++++++++++ 3 files changed, 99 insertions(+), 4 deletions(-) create mode 100644 scanner/map_bench_test.go diff --git a/scanner/README.md b/scanner/README.md index ca8f43a..a5dd60d 100644 --- a/scanner/README.md +++ b/scanner/README.md @@ -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"` @@ -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"]) diff --git a/scanner/map.go b/scanner/map.go index 11741a9..c6bbdec 100644 --- a/scanner/map.go +++ b/scanner/map.go @@ -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) { @@ -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 } @@ -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 +} diff --git a/scanner/map_bench_test.go b/scanner/map_bench_test.go new file mode 100644 index 0000000..3732b2f --- /dev/null +++ b/scanner/map_bench_test.go @@ -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) + } + }) +}