Skip to content

Commit 4cb1e9c

Browse files
authored
perf(go): optimize go perf (#3241)
## Why? #3238 introduce a performance regression for deserialization ## What does this PR do? Fix performance regression for deserialization introduced in #3238 ## Related issues #2982 #3012 ## Does this PR introduce any user-facing change? - [ ] Does this PR introduce any public API change? - [ ] Does this PR introduce any binary protocol compatibility change? ## Benchmark
1 parent 505a80a commit 4cb1e9c

4 files changed

Lines changed: 193 additions & 29 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ FORY_PYTHON_JAVA_CI=1 ENABLE_FORY_CYTHON_SERIALIZATION=1 ENABLE_FORY_DEBUG_OUTPU
151151
- All changes to `go` must pass the format check and tests.
152152
- Go implementation focuses on reflection-based and codegen-based serialization.
153153
- Set `FORY_PANIC_ON_ERROR=1` when debugging test errors to see full callstack.
154+
- You must not set `FORY_PANIC_ON_ERROR=1` when running all go tests to check whether all tests pass, some tests will check Error content, which will fail if error just panic.
154155

155156
```bash
156157
# Format code

go/fory/reader.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ type ReadContext struct {
4141
depth int // Current nesting depth for cycle detection
4242
maxDepth int // Maximum allowed nesting depth
4343
err Error // Accumulated error state for deferred checking
44+
lastTypePtr uintptr
45+
lastTypeInfo *TypeInfo
4446
}
4547

4648
// IsXlang returns whether cross-language serialization mode is enabled
@@ -180,6 +182,22 @@ func (c *ReadContext) ReadTypeId() TypeId {
180182
return TypeId(c.buffer.ReadVaruint32Small7(c.Err()))
181183
}
182184

185+
func (c *ReadContext) getTypeInfoByType(type_ reflect.Type) *TypeInfo {
186+
if type_ == nil {
187+
return nil
188+
}
189+
typePtr := typePointer(type_)
190+
if typePtr == c.lastTypePtr && c.lastTypeInfo != nil {
191+
return c.lastTypeInfo
192+
}
193+
info := c.typeResolver.getTypeInfoByType(type_)
194+
if info != nil {
195+
c.lastTypePtr = typePtr
196+
c.lastTypeInfo = info
197+
}
198+
return info
199+
}
200+
183201
// readFast reads a value using fast path based on DispatchId
184202
func (c *ReadContext) readFast(ptr unsafe.Pointer, ct DispatchId) {
185203
err := c.Err()
@@ -669,10 +687,15 @@ func (c *ReadContext) ReadValue(value reflect.Value, refMode RefMode, readType b
669687
return
670688
}
671689

690+
if typeInfo := c.getTypeInfoByType(value.Type()); typeInfo != nil && typeInfo.Serializer != nil {
691+
typeInfo.Serializer.Read(c, refMode, readType, false, value)
692+
return
693+
}
694+
672695
// For struct types, use optimized ReadStruct path when using full ref tracking and type info.
673696
// Unions use a custom serializer and must bypass ReadStruct.
674697
valueType := value.Type()
675-
if refMode == RefModeTracking && readType && !isUnionType(valueType) {
698+
if refMode == RefModeTracking && readType && !c.typeResolver.IsUnionType(valueType) {
676699
if valueType.Kind() == reflect.Struct {
677700
c.ReadStruct(value)
678701
return

go/fory/struct.go

Lines changed: 109 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ type structSerializer struct {
4444
name string
4545
type_ reflect.Type
4646
structHash int32
47+
typeID uint32
4748

4849
// Pre-sorted and categorized fields (embedded for cache locality)
4950
fieldGroup FieldGroup
@@ -2253,14 +2254,64 @@ func (s *structSerializer) Read(ctx *ReadContext, refMode RefMode, readType bool
22532254
}
22542255
}
22552256
if readType {
2256-
// Read type info - in compatible mode this returns the serializer with remote fieldDefs
2257+
if !ctx.Compatible() && s.type_ != nil {
2258+
typeID := buf.ReadVaruint32Small7(ctxErr)
2259+
if ctxErr.HasError() {
2260+
return
2261+
}
2262+
if s.typeID != 0 && typeID == s.typeID && !IsNamespacedType(TypeId(typeID)) {
2263+
s.ReadData(ctx, value)
2264+
return
2265+
}
2266+
if IsNamespacedType(TypeId(typeID)) {
2267+
// Expected type is known: skip namespace/type meta and read data directly.
2268+
ctx.TypeResolver().metaStringResolver.ReadMetaStringBytes(buf, ctxErr)
2269+
ctx.TypeResolver().metaStringResolver.ReadMetaStringBytes(buf, ctxErr)
2270+
if ctxErr.HasError() {
2271+
return
2272+
}
2273+
s.ReadData(ctx, value)
2274+
return
2275+
}
2276+
internalTypeID := TypeId(typeID & 0xFF)
2277+
if internalTypeID == COMPATIBLE_STRUCT || internalTypeID == STRUCT {
2278+
typeInfo := ctx.TypeResolver().readTypeInfoWithTypeID(buf, typeID, ctxErr)
2279+
if ctxErr.HasError() {
2280+
return
2281+
}
2282+
if structSer, ok := typeInfo.Serializer.(*structSerializer); ok && len(structSer.fieldDefs) > 0 {
2283+
structSer.ReadData(ctx, value)
2284+
return
2285+
}
2286+
if s.typeID != 0 && typeID == s.typeID {
2287+
s.ReadData(ctx, value)
2288+
return
2289+
}
2290+
}
2291+
ctx.SetError(DeserializationError("unexpected type id for struct"))
2292+
return
2293+
}
2294+
if s.type_ != nil {
2295+
serializer := ctx.TypeResolver().ReadTypeInfoForType(buf, s.type_, ctxErr)
2296+
if ctxErr.HasError() {
2297+
return
2298+
}
2299+
if serializer == nil {
2300+
ctx.SetError(DeserializationError("unexpected type id for struct"))
2301+
return
2302+
}
2303+
if structSer, ok := serializer.(*structSerializer); ok && len(structSer.fieldDefs) > 0 {
2304+
structSer.ReadData(ctx, value)
2305+
return
2306+
}
2307+
s.ReadData(ctx, value)
2308+
return
2309+
}
2310+
// Fallback: read type info based on typeID when expected type is unknown
22572311
typeID := buf.ReadVaruint32Small7(ctxErr)
22582312
internalTypeID := TypeId(typeID & 0xFF)
2259-
// Check if this is a struct type that needs type meta reading
22602313
if IsNamespacedType(TypeId(typeID)) || internalTypeID == COMPATIBLE_STRUCT || internalTypeID == STRUCT {
2261-
// For struct types in compatible mode, use the serializer from TypeInfo
22622314
typeInfo := ctx.TypeResolver().readTypeInfoWithTypeID(buf, typeID, ctxErr)
2263-
// Use the serializer from TypeInfo which has the remote field definitions
22642315
if structSer, ok := typeInfo.Serializer.(*structSerializer); ok && len(structSer.fieldDefs) > 0 {
22652316
structSer.ReadData(ctx, value)
22662317
return
@@ -2419,31 +2470,61 @@ func (s *structSerializer) ReadData(ctx *ReadContext, value reflect.Value) {
24192470
// Note: For tagged int64/uint64, we can't use unsafe reads because they need bounds checking
24202471
if len(s.fieldGroup.PrimitiveVarintFields) > 0 {
24212472
err := ctx.Err()
2422-
for _, field := range s.fieldGroup.PrimitiveVarintFields {
2423-
fieldPtr := unsafe.Add(ptr, field.Offset)
2424-
optInfo := optionalInfo{}
2425-
if field.Kind == FieldKindOptional && field.Meta != nil {
2426-
optInfo = field.Meta.OptionalInfo
2473+
if buf.remaining() >= s.fieldGroup.MaxVarintSize {
2474+
for _, field := range s.fieldGroup.PrimitiveVarintFields {
2475+
fieldPtr := unsafe.Add(ptr, field.Offset)
2476+
optInfo := optionalInfo{}
2477+
if field.Kind == FieldKindOptional && field.Meta != nil {
2478+
optInfo = field.Meta.OptionalInfo
2479+
}
2480+
switch field.DispatchId {
2481+
case PrimitiveVarint32DispatchId:
2482+
storeFieldValue(field.Kind, fieldPtr, optInfo, buf.UnsafeReadVarint32())
2483+
case PrimitiveVarint64DispatchId:
2484+
storeFieldValue(field.Kind, fieldPtr, optInfo, buf.UnsafeReadVarint64())
2485+
case PrimitiveIntDispatchId:
2486+
storeFieldValue(field.Kind, fieldPtr, optInfo, int(buf.UnsafeReadVarint64()))
2487+
case PrimitiveVarUint32DispatchId:
2488+
storeFieldValue(field.Kind, fieldPtr, optInfo, buf.UnsafeReadVaruint32())
2489+
case PrimitiveVarUint64DispatchId:
2490+
storeFieldValue(field.Kind, fieldPtr, optInfo, buf.UnsafeReadVaruint64())
2491+
case PrimitiveUintDispatchId:
2492+
storeFieldValue(field.Kind, fieldPtr, optInfo, uint(buf.UnsafeReadVaruint64()))
2493+
case PrimitiveTaggedInt64DispatchId:
2494+
// Tagged INT64: use buffer's tagged decoding (4 bytes for small, 9 for large)
2495+
storeFieldValue(field.Kind, fieldPtr, optInfo, buf.ReadTaggedInt64(err))
2496+
case PrimitiveTaggedUint64DispatchId:
2497+
// Tagged UINT64: use buffer's tagged decoding (4 bytes for small, 9 for large)
2498+
storeFieldValue(field.Kind, fieldPtr, optInfo, buf.ReadTaggedUint64(err))
2499+
}
24272500
}
2428-
switch field.DispatchId {
2429-
case PrimitiveVarint32DispatchId:
2430-
storeFieldValue(field.Kind, fieldPtr, optInfo, buf.ReadVarint32(err))
2431-
case PrimitiveVarint64DispatchId:
2432-
storeFieldValue(field.Kind, fieldPtr, optInfo, buf.ReadVarint64(err))
2433-
case PrimitiveIntDispatchId:
2434-
storeFieldValue(field.Kind, fieldPtr, optInfo, int(buf.ReadVarint64(err)))
2435-
case PrimitiveVarUint32DispatchId:
2436-
storeFieldValue(field.Kind, fieldPtr, optInfo, buf.ReadVaruint32(err))
2437-
case PrimitiveVarUint64DispatchId:
2438-
storeFieldValue(field.Kind, fieldPtr, optInfo, buf.ReadVaruint64(err))
2439-
case PrimitiveUintDispatchId:
2440-
storeFieldValue(field.Kind, fieldPtr, optInfo, uint(buf.ReadVaruint64(err)))
2441-
case PrimitiveTaggedInt64DispatchId:
2442-
// Tagged INT64: use buffer's tagged decoding (4 bytes for small, 9 for large)
2443-
storeFieldValue(field.Kind, fieldPtr, optInfo, buf.ReadTaggedInt64(err))
2444-
case PrimitiveTaggedUint64DispatchId:
2445-
// Tagged UINT64: use buffer's tagged decoding (4 bytes for small, 9 for large)
2446-
storeFieldValue(field.Kind, fieldPtr, optInfo, buf.ReadTaggedUint64(err))
2501+
} else {
2502+
for _, field := range s.fieldGroup.PrimitiveVarintFields {
2503+
fieldPtr := unsafe.Add(ptr, field.Offset)
2504+
optInfo := optionalInfo{}
2505+
if field.Kind == FieldKindOptional && field.Meta != nil {
2506+
optInfo = field.Meta.OptionalInfo
2507+
}
2508+
switch field.DispatchId {
2509+
case PrimitiveVarint32DispatchId:
2510+
storeFieldValue(field.Kind, fieldPtr, optInfo, buf.ReadVarint32(err))
2511+
case PrimitiveVarint64DispatchId:
2512+
storeFieldValue(field.Kind, fieldPtr, optInfo, buf.ReadVarint64(err))
2513+
case PrimitiveIntDispatchId:
2514+
storeFieldValue(field.Kind, fieldPtr, optInfo, int(buf.ReadVarint64(err)))
2515+
case PrimitiveVarUint32DispatchId:
2516+
storeFieldValue(field.Kind, fieldPtr, optInfo, buf.ReadVaruint32(err))
2517+
case PrimitiveVarUint64DispatchId:
2518+
storeFieldValue(field.Kind, fieldPtr, optInfo, buf.ReadVaruint64(err))
2519+
case PrimitiveUintDispatchId:
2520+
storeFieldValue(field.Kind, fieldPtr, optInfo, uint(buf.ReadVaruint64(err)))
2521+
case PrimitiveTaggedInt64DispatchId:
2522+
// Tagged INT64: use buffer's tagged decoding (4 bytes for small, 9 for large)
2523+
storeFieldValue(field.Kind, fieldPtr, optInfo, buf.ReadTaggedInt64(err))
2524+
case PrimitiveTaggedUint64DispatchId:
2525+
// Tagged UINT64: use buffer's tagged decoding (4 bytes for small, 9 for large)
2526+
storeFieldValue(field.Kind, fieldPtr, optInfo, buf.ReadTaggedUint64(err))
2527+
}
24472528
}
24482529
}
24492530
}

go/fory/type_resolver.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,9 @@ type TypeResolver struct {
187187

188188
// Fast type cache for O(1) lookup using type pointer
189189
typePointerCache map[uintptr]*TypeInfo
190+
191+
// Cache for union type detection to avoid repeated reflect.Implements in hot paths.
192+
unionTypeCache map[reflect.Type]bool
190193
}
191194

192195
func newTypeResolver(fory *Fory) *TypeResolver {
@@ -226,6 +229,7 @@ func newTypeResolver(fory *Fory) *TypeResolver {
226229
typeToTypeDef: make(map[reflect.Type]*TypeDef),
227230
defIdToTypeDef: make(map[int64]*TypeDef),
228231
typePointerCache: make(map[uintptr]*TypeInfo),
232+
unionTypeCache: make(map[reflect.Type]bool),
229233
}
230234
// base type info for encode/decode types.
231235
// composite types info will be constructed dynamically.
@@ -309,6 +313,58 @@ func (r *TypeResolver) IsXlang() bool {
309313
return r.isXlang
310314
}
311315

316+
func (r *TypeResolver) getTypeInfoByType(type_ reflect.Type) *TypeInfo {
317+
if type_ == nil {
318+
return nil
319+
}
320+
typePtr := typePointer(type_)
321+
if cachedInfo, ok := r.typePointerCache[typePtr]; ok {
322+
return cachedInfo
323+
}
324+
info, ok := r.typesInfo[type_]
325+
if !ok {
326+
return nil
327+
}
328+
if info.Serializer == nil {
329+
serializer, err := r.createSerializer(type_, false)
330+
if err != nil {
331+
return nil
332+
}
333+
info.Serializer = serializer
334+
}
335+
r.typePointerCache[typePtr] = info
336+
return info
337+
}
338+
339+
// IsUnionType returns true if the type is a union, using a local cache for performance.
340+
// Note: Fory/TypeResolver are expected to be used single-threaded.
341+
func (r *TypeResolver) IsUnionType(t reflect.Type) bool {
342+
if t == nil {
343+
return false
344+
}
345+
if v, ok := r.unionTypeCache[t]; ok {
346+
return v
347+
}
348+
base := t
349+
if info, ok := getOptionalInfo(t); ok {
350+
base = info.valueType
351+
}
352+
if v, ok := r.unionTypeCache[base]; ok {
353+
r.unionTypeCache[t] = v
354+
return v
355+
}
356+
357+
v := isUnionType(base)
358+
r.unionTypeCache[t] = v
359+
r.unionTypeCache[base] = v
360+
if base.Kind() == reflect.Ptr {
361+
r.unionTypeCache[base.Elem()] = v
362+
} else {
363+
r.unionTypeCache[reflect.PtrTo(base)] = v
364+
}
365+
return v
366+
}
367+
312368
// GetTypeInfo returns TypeInfo for the given value. This is exported for generated serializers.
313369
func (r *TypeResolver) GetTypeInfo(value reflect.Value, create bool) (*TypeInfo, error) {
314370
return r.getTypeInfo(value, create)
@@ -1236,6 +1292,9 @@ func (r *TypeResolver) registerType(
12361292
hashValue: calcTypeHash(type_), // Precomputed hash for fast lookups
12371293
NeedWriteRef: NeedWriteRef(TypeId(typeID)),
12381294
}
1295+
if structSer, ok := serializer.(*structSerializer); ok {
1296+
structSer.typeID = typeID
1297+
}
12391298
// Update resolver caches:
12401299
r.typesInfo[type_] = typeInfo // Cache by type string
12411300
if typeName != "" {

0 commit comments

Comments
 (0)