Skip to content

spanner: (*Row).ToStruct is 3x slower than (*Row).Columns #9111

@CAFxX

Description

@CAFxX

In local benchmarks (*Row).ToStruct is >3x slower than (*Row).Columns.

goos: darwin
goarch: amd64
pkg: github.com/kouzoh/go-microservices-kit/database/spanner
cpu: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz
        │ baseline-columns │           baseline-tostruct           │
        │      sec/op      │    sec/op     vs base                 │
1              211.1n ± 1%   657.9n ±  7%  +211.70% (p=0.000 n=10)
10             2.130µ ± 1%   6.107µ ±  4%  +186.71% (p=0.000 n=10)
100            15.74µ ± 1%   54.36µ ±  2%  +245.33% (p=0.000 n=10)
1000           157.7µ ± 1%   538.0µ ±  2%  +241.14% (p=0.000 n=10)
10000          1.753m ± 8%   5.591m ± 11%  +218.86% (p=0.000 n=10)
geomean        18.13µ        58.01µ        +220.03%

        │ baseline-columns │           baseline-tostruct            │
        │       B/op       │     B/op       vs base                 │
1               48.00 ± 0%     128.00 ± 0%  +166.67% (p=0.000 n=10)
10              984.0 ± 0%     1568.0 ± 0%   +59.35% (p=0.000 n=10)
100           8.320Ki ± 0%   13.812Ki ± 0%   +66.01% (p=0.000 n=10)
1000          99.41Ki ± 0%   154.12Ki ± 0%   +55.03% (p=0.000 n=10)
10000         1.287Mi ± 0%    1.822Mi ± 0%   +41.48% (p=0.000 n=10)
geomean       8.675Ki         15.00Ki        +72.95%

        │ baseline-columns │          baseline-tostruct           │
        │    allocs/op     │  allocs/op   vs base                 │
1               2.000 ± 0%    5.000 ± 0%  +150.00% (p=0.000 n=10)
10              15.00 ± 0%    36.00 ± 0%  +140.00% (p=0.000 n=10)
100             108.0 ± 0%    309.0 ± 0%  +186.11% (p=0.000 n=10)
1000           1.012k ± 0%   3.013k ± 0%  +197.73% (p=0.000 n=10)
10000          10.02k ± 0%   30.02k ± 0%  +199.63% (p=0.000 n=10)
geomean         126.9         347.0       +173.54%

Some overhead is understandable, but 3x starts to matter on hot paths.

Not sure about what should be done to improve things, short of the obvious ones:

  • do validation work common to a specific triple of (resultset schema, destination struct, lenient) only once the first time that triple is encountered, and memorize the result (including the mapping between source and destination fields) for subsequent reuse
  • otherwise minimize allocations

From a cursory look at decodeStruct it seems like doing the first one should roughly cut the time spent in that function by 40-50%.

image

cloud.google.com/go/spanner.decodeStruct
/Users/cafxx/go/pkg/mod/cloud.google.com/go/spanner@v1.53.1/value.go

  Total:       1.02s      6.49s (flat, cum) 16.06%
   3197            .          .           } 
   3198            .          .            
   3199            .          .           // decodeStruct decodes proto3.ListValue pb into struct referenced by pointer 
   3200            .          .           // ptr, according to 
   3201            .          .           // the structural information given in sppb.StructType ty. 
   3202         50ms       50ms           func decodeStruct(ty *sppb.StructType, pb *proto3.ListValue, ptr interface{}, lenient bool) error { 
   3203        100ms      100ms           	if reflect.ValueOf(ptr).IsNil() { 
   3204            .          .           		return errNilDst(ptr) 
   3205            .          .           	} 
   3206            .          .           	if ty == nil { 
   3207            .          .           		return errNilSpannerStructType() 
   3208            .          .           	} 
   3209            .          .           	// t holds the structural information of ptr. 
   3210            .      110ms           	t := reflect.TypeOf(ptr).Elem() 
   3211            .          .           	// v is the actual value that ptr points to. 
   3212        120ms      180ms           	v := reflect.ValueOf(ptr).Elem() 
   3213            .          .            
   3214         10ms      930ms           	fields, err := fieldCache.Fields(t) 
   3215         10ms       10ms           	if err != nil { 
   3216            .          .           		return ToSpannerError(err) 
   3217            .          .           	} 
   3218            .          .           	// return error if lenient is true and destination has duplicate exported columns 
   3219            .          .           	if lenient { 
   3220         10ms       10ms           		fieldNames := getAllFieldNames(v) 
   3221            .          .           		for _, f := range fieldNames { 
   3222            .          .           			if fields.Match(f) == nil { 
   3223            .          .           				return errDupGoField(ptr, f) 
   3224            .          .           			} 
   3225            .          .           		} 
   3226            .          .           	} 
   3227         10ms       90ms           	seen := map[string]bool{} 
   3228         90ms       90ms           	for i, f := range ty.Fields { 
   3229        140ms      140ms           		if f.Name == "" { 
   3230            .          .           			return errUnnamedField(ty, i) 
   3231            .          .           		} 
   3232         80ms      1.38s           		sf := fields.Match(f.Name) 
   3233         10ms       10ms           		if sf == nil { 
   3234            .          .           			if lenient { 
   3235            .          .           				continue 
   3236            .          .           			} 
   3237            .          .           			return errNoOrDupGoField(ptr, f.Name) 
   3238            .          .           		} 
   3239         70ms      200ms           		if seen[f.Name] { 
   3240            .          .           			// We don't allow duplicated field name. 
   3241            .          .           			return errDupSpannerField(f.Name, ty) 
   3242            .          .           		} 
   3243         30ms       30ms           		opts := []decodeOptions{withLenient{lenient: lenient}} 
   3244            .          .           		// Try to decode a single field. 
   3245        190ms      2.17s           		if err := decodeValue(pb.Values[i], f.Type, v.FieldByIndex(sf.Index).Addr().Interface(), opts...); err != nil { 
   3246            .          .           			return errDecodeStructField(ty, f.Name, err) 
   3247            .          .           		} 
   3248            .          .           		// Mark field f.Name as processed. 
   3249         80ms      970ms           		seen[f.Name] = true 
   3250            .          .           	} 
   3251         20ms       20ms           	return nil 
   3252            .          .           } 

Metadata

Metadata

Assignees

Labels

api: spannerIssues related to the Spanner API.priority: p1Important issue which blocks shipping the next release. Will be fixed prior to next release.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions