diff --git a/src/encoding/json/bench_test.go b/src/encoding/json/bench_test.go index 42439eb705..bd322db2e6 100644 --- a/src/encoding/json/bench_test.go +++ b/src/encoding/json/bench_test.go @@ -13,9 +13,14 @@ package json import ( "bytes" "compress/gzip" + "fmt" + "internal/testenv" "io/ioutil" "os" + "reflect" + "runtime" "strings" + "sync" "testing" ) @@ -265,3 +270,66 @@ func BenchmarkUnmapped(b *testing.B) { } }) } + +func BenchmarkTypeFieldsCache(b *testing.B) { + var maxTypes int = 1e6 + if testenv.Builder() != "" { + maxTypes = 1e3 // restrict cache sizes on builders + } + + // Dynamically generate many new types. + types := make([]reflect.Type, maxTypes) + fs := []reflect.StructField{{ + Type: reflect.TypeOf(""), + Index: []int{0}, + }} + for i := range types { + fs[0].Name = fmt.Sprintf("TypeFieldsCache%d", i) + types[i] = reflect.StructOf(fs) + } + + // clearClear clears the cache. Other JSON operations, must not be running. + clearCache := func() { + fieldCache = sync.Map{} + } + + // MissTypes tests the performance of repeated cache misses. + // This measures the time to rebuild a cache of size nt. + for nt := 1; nt <= maxTypes; nt *= 10 { + ts := types[:nt] + b.Run(fmt.Sprintf("MissTypes%d", nt), func(b *testing.B) { + nc := runtime.GOMAXPROCS(0) + for i := 0; i < b.N; i++ { + clearCache() + var wg sync.WaitGroup + for j := 0; j < nc; j++ { + wg.Add(1) + go func(j int) { + for _, t := range ts[(j*len(ts))/nc : ((j+1)*len(ts))/nc] { + cachedTypeFields(t) + } + wg.Done() + }(j) + } + wg.Wait() + } + }) + } + + // HitTypes tests the performance of repeated cache hits. + // This measures the average time of each cache lookup. + for nt := 1; nt <= maxTypes; nt *= 10 { + // Pre-warm a cache of size nt. + clearCache() + for _, t := range types[:nt] { + cachedTypeFields(t) + } + b.Run(fmt.Sprintf("HitTypes%d", nt), func(b *testing.B) { + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + cachedTypeFields(types[0]) + } + }) + }) + } +} diff --git a/src/encoding/json/encode.go b/src/encoding/json/encode.go index 68512d0225..e7e7c4b7ef 100644 --- a/src/encoding/json/encode.go +++ b/src/encoding/json/encode.go @@ -21,7 +21,6 @@ import ( "strconv" "strings" "sync" - "sync/atomic" "unicode" "unicode/utf8" ) @@ -1258,34 +1257,13 @@ func dominantField(fields []field) (field, bool) { return fields[0], true } -var fieldCache struct { - value atomic.Value // map[reflect.Type][]field - mu sync.Mutex // used only by writers -} +var fieldCache sync.Map // map[reflect.Type][]field // cachedTypeFields is like typeFields but uses a cache to avoid repeated work. func cachedTypeFields(t reflect.Type) []field { - m, _ := fieldCache.value.Load().(map[reflect.Type][]field) - f := m[t] - if f != nil { - return f + if f, ok := fieldCache.Load(t); ok { + return f.([]field) } - - // Compute fields without lock. - // Might duplicate effort but won't hold other computations back. - f = typeFields(t) - if f == nil { - f = []field{} - } - - fieldCache.mu.Lock() - m, _ = fieldCache.value.Load().(map[reflect.Type][]field) - newM := make(map[reflect.Type][]field, len(m)+1) - for k, v := range m { - newM[k] = v - } - newM[t] = f - fieldCache.value.Store(newM) - fieldCache.mu.Unlock() - return f + f, _ := fieldCache.LoadOrStore(t, typeFields(t)) + return f.([]field) }