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]