This is an automated email from the ASF dual-hosted git repository.

chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git


The following commit(s) were added to refs/heads/main by this push:
     new 4cb1e9ca2 perf(go): optimize go perf (#3241)
4cb1e9ca2 is described below

commit 4cb1e9ca22017ad09d6865f0b9e14eee238caff9
Author: Shawn Yang <[email protected]>
AuthorDate: Thu Jan 29 20:39:50 2026 +0800

    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
---
 AGENTS.md                |   1 +
 go/fory/reader.go        |  25 ++++++++-
 go/fory/struct.go        | 137 +++++++++++++++++++++++++++++++++++++----------
 go/fory/type_resolver.go |  59 ++++++++++++++++++++
 4 files changed, 193 insertions(+), 29 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md
index 6531981fd..9f3ec0faf 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -151,6 +151,7 @@ FORY_PYTHON_JAVA_CI=1 ENABLE_FORY_CYTHON_SERIALIZATION=1 
ENABLE_FORY_DEBUG_OUTPU
 - All changes to `go` must pass the format check and tests.
 - Go implementation focuses on reflection-based and codegen-based 
serialization.
 - Set `FORY_PANIC_ON_ERROR=1` when debugging test errors to see full callstack.
+- 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.
 
 ```bash
 # Format code
diff --git a/go/fory/reader.go b/go/fory/reader.go
index ed320fc99..f291e13c4 100644
--- a/go/fory/reader.go
+++ b/go/fory/reader.go
@@ -41,6 +41,8 @@ type ReadContext struct {
        depth            int           // Current nesting depth for cycle 
detection
        maxDepth         int           // Maximum allowed nesting depth
        err              Error         // Accumulated error state for deferred 
checking
+       lastTypePtr      uintptr
+       lastTypeInfo     *TypeInfo
 }
 
 // IsXlang returns whether cross-language serialization mode is enabled
@@ -180,6 +182,22 @@ func (c *ReadContext) ReadTypeId() TypeId {
        return TypeId(c.buffer.ReadVaruint32Small7(c.Err()))
 }
 
+func (c *ReadContext) getTypeInfoByType(type_ reflect.Type) *TypeInfo {
+       if type_ == nil {
+               return nil
+       }
+       typePtr := typePointer(type_)
+       if typePtr == c.lastTypePtr && c.lastTypeInfo != nil {
+               return c.lastTypeInfo
+       }
+       info := c.typeResolver.getTypeInfoByType(type_)
+       if info != nil {
+               c.lastTypePtr = typePtr
+               c.lastTypeInfo = info
+       }
+       return info
+}
+
 // readFast reads a value using fast path based on DispatchId
 func (c *ReadContext) readFast(ptr unsafe.Pointer, ct DispatchId) {
        err := c.Err()
@@ -669,10 +687,15 @@ func (c *ReadContext) ReadValue(value reflect.Value, 
refMode RefMode, readType b
                return
        }
 
+       if typeInfo := c.getTypeInfoByType(value.Type()); typeInfo != nil && 
typeInfo.Serializer != nil {
+               typeInfo.Serializer.Read(c, refMode, readType, false, value)
+               return
+       }
+
        // For struct types, use optimized ReadStruct path when using full ref 
tracking and type info.
        // Unions use a custom serializer and must bypass ReadStruct.
        valueType := value.Type()
-       if refMode == RefModeTracking && readType && !isUnionType(valueType) {
+       if refMode == RefModeTracking && readType && 
!c.typeResolver.IsUnionType(valueType) {
                if valueType.Kind() == reflect.Struct {
                        c.ReadStruct(value)
                        return
diff --git a/go/fory/struct.go b/go/fory/struct.go
index e30350320..4ffc63f69 100644
--- a/go/fory/struct.go
+++ b/go/fory/struct.go
@@ -44,6 +44,7 @@ type structSerializer struct {
        name       string
        type_      reflect.Type
        structHash int32
+       typeID     uint32
 
        // Pre-sorted and categorized fields (embedded for cache locality)
        fieldGroup FieldGroup
@@ -2253,14 +2254,64 @@ func (s *structSerializer) Read(ctx *ReadContext, 
refMode RefMode, readType bool
                }
        }
        if readType {
-               // Read type info - in compatible mode this returns the 
serializer with remote fieldDefs
+               if !ctx.Compatible() && s.type_ != nil {
+                       typeID := buf.ReadVaruint32Small7(ctxErr)
+                       if ctxErr.HasError() {
+                               return
+                       }
+                       if s.typeID != 0 && typeID == s.typeID && 
!IsNamespacedType(TypeId(typeID)) {
+                               s.ReadData(ctx, value)
+                               return
+                       }
+                       if IsNamespacedType(TypeId(typeID)) {
+                               // Expected type is known: skip namespace/type 
meta and read data directly.
+                               
ctx.TypeResolver().metaStringResolver.ReadMetaStringBytes(buf, ctxErr)
+                               
ctx.TypeResolver().metaStringResolver.ReadMetaStringBytes(buf, ctxErr)
+                               if ctxErr.HasError() {
+                                       return
+                               }
+                               s.ReadData(ctx, value)
+                               return
+                       }
+                       internalTypeID := TypeId(typeID & 0xFF)
+                       if internalTypeID == COMPATIBLE_STRUCT || 
internalTypeID == STRUCT {
+                               typeInfo := 
ctx.TypeResolver().readTypeInfoWithTypeID(buf, typeID, ctxErr)
+                               if ctxErr.HasError() {
+                                       return
+                               }
+                               if structSer, ok := 
typeInfo.Serializer.(*structSerializer); ok && len(structSer.fieldDefs) > 0 {
+                                       structSer.ReadData(ctx, value)
+                                       return
+                               }
+                               if s.typeID != 0 && typeID == s.typeID {
+                                       s.ReadData(ctx, value)
+                                       return
+                               }
+                       }
+                       ctx.SetError(DeserializationError("unexpected type id 
for struct"))
+                       return
+               }
+               if s.type_ != nil {
+                       serializer := 
ctx.TypeResolver().ReadTypeInfoForType(buf, s.type_, ctxErr)
+                       if ctxErr.HasError() {
+                               return
+                       }
+                       if serializer == nil {
+                               ctx.SetError(DeserializationError("unexpected 
type id for struct"))
+                               return
+                       }
+                       if structSer, ok := serializer.(*structSerializer); ok 
&& len(structSer.fieldDefs) > 0 {
+                               structSer.ReadData(ctx, value)
+                               return
+                       }
+                       s.ReadData(ctx, value)
+                       return
+               }
+               // Fallback: read type info based on typeID when expected type 
is unknown
                typeID := buf.ReadVaruint32Small7(ctxErr)
                internalTypeID := TypeId(typeID & 0xFF)
-               // Check if this is a struct type that needs type meta reading
                if IsNamespacedType(TypeId(typeID)) || internalTypeID == 
COMPATIBLE_STRUCT || internalTypeID == STRUCT {
-                       // For struct types in compatible mode, use the 
serializer from TypeInfo
                        typeInfo := 
ctx.TypeResolver().readTypeInfoWithTypeID(buf, typeID, ctxErr)
-                       // Use the serializer from TypeInfo which has the 
remote field definitions
                        if structSer, ok := 
typeInfo.Serializer.(*structSerializer); ok && len(structSer.fieldDefs) > 0 {
                                structSer.ReadData(ctx, value)
                                return
@@ -2419,31 +2470,61 @@ func (s *structSerializer) ReadData(ctx *ReadContext, 
value reflect.Value) {
        // Note: For tagged int64/uint64, we can't use unsafe reads because 
they need bounds checking
        if len(s.fieldGroup.PrimitiveVarintFields) > 0 {
                err := ctx.Err()
-               for _, field := range s.fieldGroup.PrimitiveVarintFields {
-                       fieldPtr := unsafe.Add(ptr, field.Offset)
-                       optInfo := optionalInfo{}
-                       if field.Kind == FieldKindOptional && field.Meta != nil 
{
-                               optInfo = field.Meta.OptionalInfo
+               if buf.remaining() >= s.fieldGroup.MaxVarintSize {
+                       for _, field := range 
s.fieldGroup.PrimitiveVarintFields {
+                               fieldPtr := unsafe.Add(ptr, field.Offset)
+                               optInfo := optionalInfo{}
+                               if field.Kind == FieldKindOptional && 
field.Meta != nil {
+                                       optInfo = field.Meta.OptionalInfo
+                               }
+                               switch field.DispatchId {
+                               case PrimitiveVarint32DispatchId:
+                                       storeFieldValue(field.Kind, fieldPtr, 
optInfo, buf.UnsafeReadVarint32())
+                               case PrimitiveVarint64DispatchId:
+                                       storeFieldValue(field.Kind, fieldPtr, 
optInfo, buf.UnsafeReadVarint64())
+                               case PrimitiveIntDispatchId:
+                                       storeFieldValue(field.Kind, fieldPtr, 
optInfo, int(buf.UnsafeReadVarint64()))
+                               case PrimitiveVarUint32DispatchId:
+                                       storeFieldValue(field.Kind, fieldPtr, 
optInfo, buf.UnsafeReadVaruint32())
+                               case PrimitiveVarUint64DispatchId:
+                                       storeFieldValue(field.Kind, fieldPtr, 
optInfo, buf.UnsafeReadVaruint64())
+                               case PrimitiveUintDispatchId:
+                                       storeFieldValue(field.Kind, fieldPtr, 
optInfo, uint(buf.UnsafeReadVaruint64()))
+                               case PrimitiveTaggedInt64DispatchId:
+                                       // Tagged INT64: use buffer's tagged 
decoding (4 bytes for small, 9 for large)
+                                       storeFieldValue(field.Kind, fieldPtr, 
optInfo, buf.ReadTaggedInt64(err))
+                               case PrimitiveTaggedUint64DispatchId:
+                                       // Tagged UINT64: use buffer's tagged 
decoding (4 bytes for small, 9 for large)
+                                       storeFieldValue(field.Kind, fieldPtr, 
optInfo, buf.ReadTaggedUint64(err))
+                               }
                        }
-                       switch field.DispatchId {
-                       case PrimitiveVarint32DispatchId:
-                               storeFieldValue(field.Kind, fieldPtr, optInfo, 
buf.ReadVarint32(err))
-                       case PrimitiveVarint64DispatchId:
-                               storeFieldValue(field.Kind, fieldPtr, optInfo, 
buf.ReadVarint64(err))
-                       case PrimitiveIntDispatchId:
-                               storeFieldValue(field.Kind, fieldPtr, optInfo, 
int(buf.ReadVarint64(err)))
-                       case PrimitiveVarUint32DispatchId:
-                               storeFieldValue(field.Kind, fieldPtr, optInfo, 
buf.ReadVaruint32(err))
-                       case PrimitiveVarUint64DispatchId:
-                               storeFieldValue(field.Kind, fieldPtr, optInfo, 
buf.ReadVaruint64(err))
-                       case PrimitiveUintDispatchId:
-                               storeFieldValue(field.Kind, fieldPtr, optInfo, 
uint(buf.ReadVaruint64(err)))
-                       case PrimitiveTaggedInt64DispatchId:
-                               // Tagged INT64: use buffer's tagged decoding 
(4 bytes for small, 9 for large)
-                               storeFieldValue(field.Kind, fieldPtr, optInfo, 
buf.ReadTaggedInt64(err))
-                       case PrimitiveTaggedUint64DispatchId:
-                               // Tagged UINT64: use buffer's tagged decoding 
(4 bytes for small, 9 for large)
-                               storeFieldValue(field.Kind, fieldPtr, optInfo, 
buf.ReadTaggedUint64(err))
+               } else {
+                       for _, field := range 
s.fieldGroup.PrimitiveVarintFields {
+                               fieldPtr := unsafe.Add(ptr, field.Offset)
+                               optInfo := optionalInfo{}
+                               if field.Kind == FieldKindOptional && 
field.Meta != nil {
+                                       optInfo = field.Meta.OptionalInfo
+                               }
+                               switch field.DispatchId {
+                               case PrimitiveVarint32DispatchId:
+                                       storeFieldValue(field.Kind, fieldPtr, 
optInfo, buf.ReadVarint32(err))
+                               case PrimitiveVarint64DispatchId:
+                                       storeFieldValue(field.Kind, fieldPtr, 
optInfo, buf.ReadVarint64(err))
+                               case PrimitiveIntDispatchId:
+                                       storeFieldValue(field.Kind, fieldPtr, 
optInfo, int(buf.ReadVarint64(err)))
+                               case PrimitiveVarUint32DispatchId:
+                                       storeFieldValue(field.Kind, fieldPtr, 
optInfo, buf.ReadVaruint32(err))
+                               case PrimitiveVarUint64DispatchId:
+                                       storeFieldValue(field.Kind, fieldPtr, 
optInfo, buf.ReadVaruint64(err))
+                               case PrimitiveUintDispatchId:
+                                       storeFieldValue(field.Kind, fieldPtr, 
optInfo, uint(buf.ReadVaruint64(err)))
+                               case PrimitiveTaggedInt64DispatchId:
+                                       // Tagged INT64: use buffer's tagged 
decoding (4 bytes for small, 9 for large)
+                                       storeFieldValue(field.Kind, fieldPtr, 
optInfo, buf.ReadTaggedInt64(err))
+                               case PrimitiveTaggedUint64DispatchId:
+                                       // Tagged UINT64: use buffer's tagged 
decoding (4 bytes for small, 9 for large)
+                                       storeFieldValue(field.Kind, fieldPtr, 
optInfo, buf.ReadTaggedUint64(err))
+                               }
                        }
                }
        }
diff --git a/go/fory/type_resolver.go b/go/fory/type_resolver.go
index 9bc920a75..5b5b7db74 100644
--- a/go/fory/type_resolver.go
+++ b/go/fory/type_resolver.go
@@ -187,6 +187,9 @@ type TypeResolver struct {
 
        // Fast type cache for O(1) lookup using type pointer
        typePointerCache map[uintptr]*TypeInfo
+
+       // Cache for union type detection to avoid repeated reflect.Implements 
in hot paths.
+       unionTypeCache map[reflect.Type]bool
 }
 
 func newTypeResolver(fory *Fory) *TypeResolver {
@@ -226,6 +229,7 @@ func newTypeResolver(fory *Fory) *TypeResolver {
                typeToTypeDef:    make(map[reflect.Type]*TypeDef),
                defIdToTypeDef:   make(map[int64]*TypeDef),
                typePointerCache: make(map[uintptr]*TypeInfo),
+               unionTypeCache:   make(map[reflect.Type]bool),
        }
        // base type info for encode/decode types.
        // composite types info will be constructed dynamically.
@@ -309,6 +313,58 @@ func (r *TypeResolver) IsXlang() bool {
        return r.isXlang
 }
 
+func (r *TypeResolver) getTypeInfoByType(type_ reflect.Type) *TypeInfo {
+       if type_ == nil {
+               return nil
+       }
+       typePtr := typePointer(type_)
+       if cachedInfo, ok := r.typePointerCache[typePtr]; ok {
+               return cachedInfo
+       }
+       info, ok := r.typesInfo[type_]
+       if !ok {
+               return nil
+       }
+       if info.Serializer == nil {
+               serializer, err := r.createSerializer(type_, false)
+               if err != nil {
+                       return nil
+               }
+               info.Serializer = serializer
+       }
+       r.typePointerCache[typePtr] = info
+       return info
+}
+
+// IsUnionType returns true if the type is a union, using a local cache for 
performance.
+// Note: Fory/TypeResolver are expected to be used single-threaded.
+func (r *TypeResolver) IsUnionType(t reflect.Type) bool {
+       if t == nil {
+               return false
+       }
+       if v, ok := r.unionTypeCache[t]; ok {
+               return v
+       }
+       base := t
+       if info, ok := getOptionalInfo(t); ok {
+               base = info.valueType
+       }
+       if v, ok := r.unionTypeCache[base]; ok {
+               r.unionTypeCache[t] = v
+               return v
+       }
+
+       v := isUnionType(base)
+       r.unionTypeCache[t] = v
+       r.unionTypeCache[base] = v
+       if base.Kind() == reflect.Ptr {
+               r.unionTypeCache[base.Elem()] = v
+       } else {
+               r.unionTypeCache[reflect.PtrTo(base)] = v
+       }
+       return v
+}
+
 // GetTypeInfo returns TypeInfo for the given value. This is exported for 
generated serializers.
 func (r *TypeResolver) GetTypeInfo(value reflect.Value, create bool) 
(*TypeInfo, error) {
        return r.getTypeInfo(value, create)
@@ -1236,6 +1292,9 @@ func (r *TypeResolver) registerType(
                hashValue:    calcTypeHash(type_),  // Precomputed hash for 
fast lookups
                NeedWriteRef: NeedWriteRef(TypeId(typeID)),
        }
+       if structSer, ok := serializer.(*structSerializer); ok {
+               structSer.typeID = typeID
+       }
        // Update resolver caches:
        r.typesInfo[type_] = typeInfo // Cache by type string
        if typeName != "" {


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to