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 455628477 perf(go): optimize go struct fields serialization perf
(#3120)
455628477 is described below
commit 455628477ba6bef2d565b961db7077b180fd26b3
Author: Shawn Yang <[email protected]>
AuthorDate: Sun Jan 11 11:28:20 2026 +0800
perf(go): optimize go struct fields serialization perf (#3120)
## Why?
## What does this PR do?
Fix performance regression introduced in #3113
## Related issues
#3113
## 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
---
go/fory/buffer.go | 41 +++++++++
go/fory/field_info.go | 117 ++++++++++++++++---------
go/fory/primitive.go | 34 ++++++++
go/fory/struct.go | 238 +++++++++++++++++++++++++-------------------------
go/fory/types.go | 52 ++++++-----
5 files changed, 296 insertions(+), 186 deletions(-)
diff --git a/go/fory/buffer.go b/go/fory/buffer.go
index 13ec17f4a..74e4498d7 100644
--- a/go/fory/buffer.go
+++ b/go/fory/buffer.go
@@ -1539,6 +1539,47 @@ func (b *ByteBuffer) UnsafePutInt64(offset int, value
int64) int {
return 8
}
+// UnsafePutTaggedInt64 writes int64 using tagged encoding at the given offset.
+// Caller must have ensured capacity (9 bytes max).
+// Returns the number of bytes written (4 or 9).
+//
+//go:inline
+func (b *ByteBuffer) UnsafePutTaggedInt64(offset int, value int64) int {
+ const halfMinIntValue int64 = -1073741824 // INT32_MIN / 2
+ const halfMaxIntValue int64 = 1073741823 // INT32_MAX / 2
+ if value >= halfMinIntValue && value <= halfMaxIntValue {
+ binary.LittleEndian.PutUint32(b.data[offset:],
uint32(int32(value)<<1))
+ return 4
+ }
+ b.data[offset] = 0b1
+ if isLittleEndian {
+ *(*int64)(unsafe.Pointer(&b.data[offset+1])) = value
+ } else {
+ binary.LittleEndian.PutUint64(b.data[offset+1:], uint64(value))
+ }
+ return 9
+}
+
+// UnsafePutTaggedUint64 writes uint64 using tagged encoding at the given
offset.
+// Caller must have ensured capacity (9 bytes max).
+// Returns the number of bytes written (4 or 9).
+//
+//go:inline
+func (b *ByteBuffer) UnsafePutTaggedUint64(offset int, value uint64) int {
+ const maxSmallValue uint64 = 0x7fffffff // INT32_MAX as u64
+ if value <= maxSmallValue {
+ binary.LittleEndian.PutUint32(b.data[offset:], uint32(value)<<1)
+ return 4
+ }
+ b.data[offset] = 0b1
+ if isLittleEndian {
+ *(*uint64)(unsafe.Pointer(&b.data[offset+1])) = value
+ } else {
+ binary.LittleEndian.PutUint64(b.data[offset+1:], value)
+ }
+ return 9
+}
+
// ReadVaruint32Small7 reads a varuint32 in small-7 format with error checking
func (b *ByteBuffer) ReadVaruint32Small7(err *Error) uint32 {
if b.readerIndex >= len(b.data) {
diff --git a/go/fory/field_info.go b/go/fory/field_info.go
index cac8ae446..7739894ec 100644
--- a/go/fory/field_info.go
+++ b/go/fory/field_info.go
@@ -24,27 +24,31 @@ import (
"strings"
)
-// FieldInfo stores field metadata computed ENTIRELY at init time.
-// All flags and decisions are pre-computed to eliminate runtime checks.
-type FieldInfo struct {
+// PrimitiveFieldInfo contains only the fields needed for hot primitive
serialization loops.
+// This minimal struct improves cache efficiency during iteration.
+// Size: 16 bytes (vs full FieldInfo)
+type PrimitiveFieldInfo struct {
+ Offset uintptr // Field offset for unsafe access
+ DispatchId DispatchId // Type dispatch ID
+ WriteOffset uint8 // Offset within fixed-fields buffer (0-255,
sufficient for fixed primitives)
+}
+
+// FieldMeta contains cold/rarely-accessed field metadata.
+// Accessed via pointer from FieldInfo to keep FieldInfo small for cache
efficiency.
+type FieldMeta struct {
Name string
- Offset uintptr
Type reflect.Type
- DispatchId DispatchId
TypeId TypeId // Fory type ID for the serializer
- Serializer Serializer
Nullable bool
FieldIndex int // -1 if field doesn't exist in current struct (for
compatible mode)
FieldDef FieldDef // original FieldDef from remote TypeDef (for
compatible mode skip)
- // Pre-computed sizes and offsets (for fixed primitives)
- FixedSize int // 0 if not fixed-size, else 1/2/4/8
- WriteOffset int // Offset within fixed-fields buffer region (sum of
preceding field sizes)
+ // Pre-computed sizes (for fixed primitives)
+ FixedSize int // 0 if not fixed-size, else 1/2/4/8
// Pre-computed flags for serialization (computed at init time)
- RefMode RefMode // ref mode for serializer.Write/Read
- WriteType bool // whether to write type info (true for struct
fields in compatible mode)
- HasGenerics bool // whether element types are known from TypeDef
(for container fields)
+ WriteType bool // whether to write type info (true for struct fields
in compatible mode)
+ HasGenerics bool // whether element types are known from TypeDef (for
container fields)
// Tag-based configuration (from fory struct tags)
TagID int // -1 = use field name, >=0 = use tag ID
@@ -53,9 +57,21 @@ type FieldInfo struct {
TagRef bool // The ref value from fory tag (only valid if
TagRefSet is true)
TagNullableSet bool // Whether nullable was explicitly set via fory tag
TagNullable bool // The nullable value from fory tag (only valid if
TagNullableSet is true)
+}
- // Pre-computed type flags (computed at init time to avoid runtime
reflection)
- IsPtr bool // True if field.Type.Kind() == reflect.Ptr
+// FieldInfo stores field metadata computed ENTIRELY at init time.
+// Hot fields are kept inline for cache efficiency, cold fields accessed via
Meta pointer.
+type FieldInfo struct {
+ // Hot fields - accessed frequently during serialization
+ Offset uintptr // Field offset for unsafe access
+ DispatchId DispatchId // Type dispatch ID
+ WriteOffset int // Offset within fixed-fields buffer region (sum
of preceding field sizes)
+ RefMode RefMode // ref mode for serializer.Write/Read
+ IsPtr bool // True if field.Type.Kind() == reflect.Ptr
+ Serializer Serializer // Serializer for this field
+
+ // Cold fields - accessed less frequently
+ Meta *FieldMeta
}
// FieldGroup holds categorized and sorted fields for optimized serialization.
@@ -65,6 +81,11 @@ type FieldInfo struct {
// - VarintFields: non-nullable varint primitives (varint32/64, var_uint32/64,
tagged_int64/uint64)
// - RemainingFields: all other fields (nullable primitives, strings,
collections, structs, etc.)
type FieldGroup struct {
+ // Primitive field slices - minimal data for fast iteration in hot loops
+ PrimitiveFixedFields []PrimitiveFieldInfo // Minimal fixed field info
for hot loop
+ PrimitiveVarintFields []PrimitiveFieldInfo // Minimal varint field info
for hot loop
+
+ // Full field info for remaining fields and fallback paths
FixedFields []FieldInfo // Non-nullable fixed-size primitives
VarintFields []FieldInfo // Non-nullable varint primitives
RemainingFields []FieldInfo // All other fields
@@ -100,19 +121,19 @@ func (g *FieldGroup) DebugPrint(typeName string) {
for i := range g.FixedFields {
f := &g.FixedFields[i]
fmt.Printf("[Go] [%d] %s -> dispatchId=%d, typeId=%d,
size=%d, nullable=%v\n",
- i, f.Name, f.DispatchId, f.TypeId, f.FixedSize,
f.Nullable)
+ i, f.Meta.Name, f.DispatchId, f.Meta.TypeId,
f.Meta.FixedSize, f.Meta.Nullable)
}
fmt.Printf("[Go] Go sorted varintFields (%d):\n", len(g.VarintFields))
for i := range g.VarintFields {
f := &g.VarintFields[i]
fmt.Printf("[Go] [%d] %s -> dispatchId=%d, typeId=%d,
nullable=%v\n",
- i, f.Name, f.DispatchId, f.TypeId, f.Nullable)
+ i, f.Meta.Name, f.DispatchId, f.Meta.TypeId,
f.Meta.Nullable)
}
fmt.Printf("[Go] Go sorted remainingFields (%d):\n",
len(g.RemainingFields))
for i := range g.RemainingFields {
f := &g.RemainingFields[i]
fmt.Printf("[Go] [%d] %s -> dispatchId=%d, typeId=%d,
nullable=%v\n",
- i, f.Name, f.DispatchId, f.TypeId, f.Nullable)
+ i, f.Meta.Name, f.DispatchId, f.Meta.TypeId,
f.Meta.Nullable)
}
fmt.Printf("[Go] ===========================================\n")
}
@@ -126,11 +147,11 @@ func GroupFields(fields []FieldInfo) FieldGroup {
// Categorize fields
for i := range fields {
field := &fields[i]
- if isFixedSizePrimitive(field.DispatchId, field.Nullable) {
+ if isFixedSizePrimitive(field.DispatchId, field.Meta.Nullable) {
// Non-nullable fixed-size primitives only
- field.FixedSize =
getFixedSizeByDispatchId(field.DispatchId)
+ field.Meta.FixedSize =
getFixedSizeByDispatchId(field.DispatchId)
g.FixedFields = append(g.FixedFields, *field)
- } else if isVarintPrimitive(field.DispatchId, field.Nullable) {
+ } else if isVarintPrimitive(field.DispatchId,
field.Meta.Nullable) {
// Non-nullable varint primitives only
g.VarintFields = append(g.VarintFields, *field)
} else {
@@ -142,19 +163,25 @@ func GroupFields(fields []FieldInfo) FieldGroup {
// Sort fixedFields: size desc, typeId desc, name asc
sort.SliceStable(g.FixedFields, func(i, j int) bool {
fi, fj := &g.FixedFields[i], &g.FixedFields[j]
- if fi.FixedSize != fj.FixedSize {
- return fi.FixedSize > fj.FixedSize // size descending
+ if fi.Meta.FixedSize != fj.Meta.FixedSize {
+ return fi.Meta.FixedSize > fj.Meta.FixedSize // size
descending
}
- if fi.TypeId != fj.TypeId {
- return fi.TypeId > fj.TypeId // typeId descending
+ if fi.Meta.TypeId != fj.Meta.TypeId {
+ return fi.Meta.TypeId > fj.Meta.TypeId // typeId
descending
}
- return fi.Name < fj.Name // name ascending
+ return fi.Meta.Name < fj.Meta.Name // name ascending
})
- // Compute WriteOffset after sorting
+ // Compute WriteOffset after sorting and build primitive field slice
+ g.PrimitiveFixedFields = make([]PrimitiveFieldInfo, len(g.FixedFields))
for i := range g.FixedFields {
g.FixedFields[i].WriteOffset = g.FixedSize
- g.FixedSize += g.FixedFields[i].FixedSize
+ g.PrimitiveFixedFields[i] = PrimitiveFieldInfo{
+ Offset: g.FixedFields[i].Offset,
+ DispatchId: g.FixedFields[i].DispatchId,
+ WriteOffset: uint8(g.FixedSize),
+ }
+ g.FixedSize += g.FixedFields[i].Meta.FixedSize
}
// Sort varintFields: underlying type size desc, typeId desc, name asc
@@ -166,15 +193,21 @@ func GroupFields(fields []FieldInfo) FieldGroup {
if sizeI != sizeJ {
return sizeI > sizeJ // size descending
}
- if fi.TypeId != fj.TypeId {
- return fi.TypeId > fj.TypeId // typeId descending
+ if fi.Meta.TypeId != fj.Meta.TypeId {
+ return fi.Meta.TypeId > fj.Meta.TypeId // typeId
descending
}
- return fi.Name < fj.Name // name ascending
+ return fi.Meta.Name < fj.Meta.Name // name ascending
})
- // Compute maxVarintSize
+ // Compute maxVarintSize and build primitive varint field slice
+ g.PrimitiveVarintFields = make([]PrimitiveFieldInfo,
len(g.VarintFields))
for i := range g.VarintFields {
g.MaxVarintSize +=
getVarintMaxSizeByDispatchId(g.VarintFields[i].DispatchId)
+ g.PrimitiveVarintFields[i] = PrimitiveFieldInfo{
+ Offset: g.VarintFields[i].Offset,
+ DispatchId: g.VarintFields[i].DispatchId,
+ // WriteOffset not used for varint fields (variable
length)
+ }
}
// Sort remainingFields: nullable primitives first (by
primitiveComparator),
@@ -192,8 +225,8 @@ func GroupFields(fields []FieldInfo) FieldGroup {
// Within other internal types category (STRING, BINARY, LIST,
SET, MAP),
// sort by typeId then by sort key (tagID if available,
otherwise name).
if catI == 1 {
- if fi.TypeId != fj.TypeId {
- return fi.TypeId < fj.TypeId
+ if fi.Meta.TypeId != fj.Meta.TypeId {
+ return fi.Meta.TypeId < fj.Meta.TypeId
}
return getFieldSortKey(fi) < getFieldSortKey(fj)
}
@@ -215,7 +248,7 @@ func fieldHasNonPrimitiveSerializer(field *FieldInfo) bool {
// all require special serialization and should not use the primitive
fast path
// Note: ENUM uses unsigned Varuint32Small7 for ordinals, not signed
zigzag varint
// Use internal type ID (low 8 bits) since registered types have
composite TypeIds like (userID << 8) | internalID
- internalTypeId := TypeId(field.TypeId & 0xFF)
+ internalTypeId := TypeId(field.Meta.TypeId & 0xFF)
switch internalTypeId {
case ENUM, NAMED_ENUM, NAMED_STRUCT, NAMED_COMPATIBLE_STRUCT, NAMED_EXT:
return true
@@ -229,7 +262,7 @@ func isEnumField(field *FieldInfo) bool {
if field.Serializer == nil {
return false
}
- internalTypeId := field.TypeId & 0xFF
+ internalTypeId := field.Meta.TypeId & 0xFF
return internalTypeId == ENUM || internalTypeId == NAMED_ENUM
}
@@ -241,7 +274,7 @@ func getFieldCategory(field *FieldInfo) int {
if isNullableFixedSizePrimitive(field.DispatchId) ||
isNullableVarintPrimitive(field.DispatchId) {
return 0
}
- internalId := field.TypeId & 0xFF
+ internalId := field.Meta.TypeId & 0xFF
switch TypeId(internalId) {
case STRING, BINARY, LIST, SET, MAP:
// Internal types: sorted by typeId, then name
@@ -267,10 +300,10 @@ func comparePrimitiveFields(fi, fj *FieldInfo) bool {
if sizeI != sizeJ {
return sizeI > sizeJ // size descending
}
- if fi.TypeId != fj.TypeId {
- return fi.TypeId > fj.TypeId // typeId descending
+ if fi.Meta.TypeId != fj.Meta.TypeId {
+ return fi.Meta.TypeId > fj.Meta.TypeId // typeId descending
}
- return fi.Name < fj.Name // name ascending
+ return fi.Meta.Name < fj.Meta.Name // name ascending
}
// getNullableFixedSize returns the fixed size for nullable fixed primitives
@@ -532,10 +565,10 @@ func (t triple) getSortKey() string {
// If TagID >= 0, returns the tag ID as string (for tag-based sorting).
// Otherwise returns the field name (which is already snake_case).
func getFieldSortKey(f *FieldInfo) string {
- if f.TagID >= 0 {
- return fmt.Sprintf("%d", f.TagID)
+ if f.Meta.TagID >= 0 {
+ return fmt.Sprintf("%d", f.Meta.TagID)
}
- return f.Name
+ return f.Meta.Name
}
// sortFields sorts fields with nullable information to match Java's field
ordering.
diff --git a/go/fory/primitive.go b/go/fory/primitive.go
index b0bf96767..d8978b88a 100644
--- a/go/fory/primitive.go
+++ b/go/fory/primitive.go
@@ -19,6 +19,7 @@ package fory
import (
"reflect"
+ "unsafe"
)
// ============================================================================
@@ -563,3 +564,36 @@ func (s float64Serializer) Read(ctx *ReadContext, refMode
RefMode, readType bool
func (s float64Serializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode,
typeInfo *TypeInfo, value reflect.Value) {
s.Read(ctx, refMode, false, false, value)
}
+
+// ============================================================================
+// Notnull Pointer Helper Functions for Varint Types
+// These are used by struct serializer for the rare case of *T with
nullable=false
+// ============================================================================
+
+// writeNotnullVarintPtrUnsafe writes a notnull pointer varint type at the
given offset.
+// Used by struct serializer for rare notnull pointer types.
+// Returns the number of bytes written.
+//
+//go:inline
+func writeNotnullVarintPtrUnsafe(buf *ByteBuffer, offset int, fieldPtr
unsafe.Pointer, dispatchId DispatchId) int {
+ switch dispatchId {
+ case NotnullVarint32PtrDispatchId:
+ return buf.UnsafePutVarInt32(offset, **(**int32)(fieldPtr))
+ case NotnullVarint64PtrDispatchId:
+ return buf.UnsafePutVarInt64(offset, **(**int64)(fieldPtr))
+ case NotnullIntPtrDispatchId:
+ return buf.UnsafePutVarInt64(offset, int64(**(**int)(fieldPtr)))
+ case NotnullVarUint32PtrDispatchId:
+ return buf.UnsafePutVaruint32(offset, **(**uint32)(fieldPtr))
+ case NotnullVarUint64PtrDispatchId:
+ return buf.UnsafePutVaruint64(offset, **(**uint64)(fieldPtr))
+ case NotnullUintPtrDispatchId:
+ return buf.UnsafePutVaruint64(offset,
uint64(**(**uint)(fieldPtr)))
+ case NotnullTaggedInt64PtrDispatchId:
+ return buf.UnsafePutTaggedInt64(offset, **(**int64)(fieldPtr))
+ case NotnullTaggedUint64PtrDispatchId:
+ return buf.UnsafePutTaggedUint64(offset, **(**uint64)(fieldPtr))
+ default:
+ return 0
+ }
+}
diff --git a/go/fory/struct.go b/go/fory/struct.go
index b8f3c7765..e38f51a4e 100644
--- a/go/fory/struct.go
+++ b/go/fory/struct.go
@@ -306,27 +306,29 @@ func (s *structSerializer) initFields(typeResolver
*TypeResolver) error {
}
fieldInfo := FieldInfo{
- Name: SnakeCase(field.Name),
- Offset: field.Offset,
- Type: fieldType,
- DispatchId: dispatchId,
- TypeId: fieldTypeId,
- Serializer: fieldSerializer,
- Nullable: nullableFlag, // Use same logic as
TypeDef's nullable flag for consistent ref handling
- FieldIndex: i,
- RefMode: refMode,
- WriteType: writeType,
- HasGenerics: isCollectionType(fieldTypeId), //
Container fields have declared element types
- TagID: foryTag.ID,
- HasForyTag: foryTag.HasTag,
- TagRefSet: foryTag.RefSet,
- TagRef: foryTag.Ref,
- TagNullableSet: foryTag.NullableSet,
- TagNullable: foryTag.Nullable,
- IsPtr: fieldType.Kind() == reflect.Ptr,
+ Offset: field.Offset,
+ DispatchId: dispatchId,
+ RefMode: refMode,
+ IsPtr: fieldType.Kind() == reflect.Ptr,
+ Serializer: fieldSerializer,
+ Meta: &FieldMeta{
+ Name: SnakeCase(field.Name),
+ Type: fieldType,
+ TypeId: fieldTypeId,
+ Nullable: nullableFlag, // Use same logic
as TypeDef's nullable flag for consistent ref handling
+ FieldIndex: i,
+ WriteType: writeType,
+ HasGenerics: isCollectionType(fieldTypeId),
// Container fields have declared element types
+ TagID: foryTag.ID,
+ HasForyTag: foryTag.HasTag,
+ TagRefSet: foryTag.RefSet,
+ TagRef: foryTag.Ref,
+ TagNullableSet: foryTag.NullableSet,
+ TagNullable: foryTag.Nullable,
+ },
}
fields = append(fields, fieldInfo)
- fieldNames = append(fieldNames, fieldInfo.Name)
+ fieldNames = append(fieldNames, fieldInfo.Meta.Name)
serializers = append(serializers, fieldSerializer)
typeIds = append(typeIds, fieldTypeId)
nullables = append(nullables, nullableFlag)
@@ -341,8 +343,8 @@ func (s *structSerializer) initFields(typeResolver
*TypeResolver) error {
}
sort.SliceStable(fields, func(i, j int) bool {
- oi, okI := order[fields[i].Name]
- oj, okJ := order[fields[j].Name]
+ oi, okI := order[fields[i].Meta.Name]
+ oj, okJ := order[fields[j].Meta.Name]
switch {
case okI && okJ:
return oi < oj
@@ -406,19 +408,21 @@ func (s *structSerializer)
initFieldsFromTypeDef(typeResolver *TypeResolver) err
}
fieldInfo := FieldInfo{
- Name: def.name,
- Offset: 0,
- Type: remoteType,
- DispatchId: dispatchId,
- TypeId: fieldTypeId,
- Serializer: fieldSerializer,
- Nullable: def.nullable, // Use remote
nullable flag
- FieldIndex: -1, // Mark as
non-existent field to discard data
- FieldDef: def, // Save original
FieldDef for skipping
- RefMode: refMode,
- WriteType: writeType,
- HasGenerics: isCollectionType(fieldTypeId), //
Container fields have declared element types
- IsPtr: remoteType != nil &&
remoteType.Kind() == reflect.Ptr,
+ Offset: 0,
+ DispatchId: dispatchId,
+ RefMode: refMode,
+ IsPtr: remoteType != nil &&
remoteType.Kind() == reflect.Ptr,
+ Serializer: fieldSerializer,
+ Meta: &FieldMeta{
+ Name: def.name,
+ Type: remoteType,
+ TypeId: fieldTypeId,
+ Nullable: def.nullable, // Use
remote nullable flag
+ FieldIndex: -1, // Mark as
non-existent field to discard data
+ FieldDef: def, // Save
original FieldDef for skipping
+ WriteType: writeType,
+ HasGenerics:
isCollectionType(fieldTypeId), // Container fields have declared element types
+ },
}
fields = append(fields, fieldInfo)
}
@@ -713,21 +717,23 @@ func (s *structSerializer)
initFieldsFromTypeDef(typeResolver *TypeResolver) err
}
fieldInfo := FieldInfo{
- Name: fieldName,
- Offset: offset,
- Type: fieldType,
- DispatchId: dispatchId,
- TypeId: fieldTypeId,
- Serializer: fieldSerializer,
- Nullable: def.nullable, // Use remote nullable flag
- FieldIndex: fieldIndex,
- FieldDef: def, // Save original FieldDef for skipping
- RefMode: refMode,
- WriteType: writeType,
- HasGenerics: isCollectionType(fieldTypeId), //
Container fields have declared element types
- TagID: def.tagID,
- HasForyTag: def.tagID >= 0,
- IsPtr: fieldType != nil && fieldType.Kind() ==
reflect.Ptr,
+ Offset: offset,
+ DispatchId: dispatchId,
+ RefMode: refMode,
+ IsPtr: fieldType != nil && fieldType.Kind() ==
reflect.Ptr,
+ Serializer: fieldSerializer,
+ Meta: &FieldMeta{
+ Name: fieldName,
+ Type: fieldType,
+ TypeId: fieldTypeId,
+ Nullable: def.nullable, // Use remote
nullable flag
+ FieldIndex: fieldIndex,
+ FieldDef: def, // Save original FieldDef for
skipping
+ WriteType: writeType,
+ HasGenerics: isCollectionType(fieldTypeId), //
Container fields have declared element types
+ TagID: def.tagID,
+ HasForyTag: def.tagID >= 0,
+ },
}
fields = append(fields, fieldInfo)
}
@@ -749,7 +755,7 @@ func (s *structSerializer)
initFieldsFromTypeDef(typeResolver *TypeResolver) err
// When typeDefDiffers is false, we can use grouped reading for better
performance
s.typeDefDiffers = false
for i, field := range fields {
- if field.FieldIndex < 0 {
+ if field.Meta.FieldIndex < 0 {
// Field exists in remote TypeDef but not locally
s.typeDefDiffers = true
break
@@ -757,7 +763,7 @@ func (s *structSerializer)
initFieldsFromTypeDef(typeResolver *TypeResolver) err
// Check if nullable flag differs between remote and local
// Remote nullable is stored in fieldDefs[i].nullable
// Local nullable is determined by whether the Go field is a
pointer type
- if i < len(s.fieldDefs) && field.FieldIndex >= 0 {
+ if i < len(s.fieldDefs) && field.Meta.FieldIndex >= 0 {
remoteNullable := s.fieldDefs[i].nullable
// Check if local Go field is a pointer type (can be
nil = nullable)
localNullable := field.IsPtr
@@ -784,7 +790,7 @@ func (s *structSerializer) computeHash() int32 {
if field.Serializer == nil {
typeId = UNKNOWN
} else {
- typeId = field.TypeId
+ typeId = field.Meta.TypeId
// Check if this is an enum serializer (directly or
wrapped in ptrToValueSerializer)
if _, ok := field.Serializer.(*enumSerializer); ok {
isEnumField = true
@@ -802,8 +808,8 @@ func (s *structSerializer) computeHash() int32 {
typeId = UNKNOWN
}
// For fixed-size arrays with primitive elements, use
primitive array type IDs
- if field.Type.Kind() == reflect.Array {
- elemKind := field.Type.Elem().Kind()
+ if field.Meta.Type.Kind() == reflect.Array {
+ elemKind := field.Meta.Type.Elem().Kind()
switch elemKind {
case reflect.Int8:
typeId = INT8_ARRAY
@@ -820,11 +826,11 @@ func (s *structSerializer) computeHash() int32 {
default:
typeId = LIST
}
- } else if field.Type.Kind() == reflect.Slice {
+ } else if field.Meta.Type.Kind() == reflect.Slice {
typeId = LIST
- } else if field.Type.Kind() == reflect.Map {
+ } else if field.Meta.Type.Kind() == reflect.Map {
// map[T]bool is used to represent a Set in Go
- if field.Type.Elem().Kind() == reflect.Bool {
+ if field.Meta.Type.Elem().Kind() ==
reflect.Bool {
typeId = SET
} else {
typeId = MAP
@@ -837,22 +843,22 @@ func (s *structSerializer) computeHash() int32 {
// - Primitives are always non-nullable
// - Can be overridden by explicit fory tag
nullable := false // Default to nullable=false for xlang mode
- if field.TagNullableSet {
+ if field.Meta.TagNullableSet {
// Use explicit tag value if set
- nullable = field.TagNullable
+ nullable = field.Meta.TagNullable
}
// Primitives are never nullable, regardless of tag
- if isNonNullablePrimitiveKind(field.Type.Kind()) &&
!isEnumField {
+ if isNonNullablePrimitiveKind(field.Meta.Type.Kind()) &&
!isEnumField {
nullable = false
}
fields = append(fields, FieldFingerprintInfo{
- FieldID: field.TagID,
- FieldName: SnakeCase(field.Name),
+ FieldID: field.Meta.TagID,
+ FieldName: SnakeCase(field.Meta.Name),
TypeID: typeId,
// Ref is based on explicit tag annotation only, NOT
runtime ref_tracking config
// This allows fingerprint to be computed at compile
time for C++/Rust
- Ref: field.TagRefSet && field.TagRef,
+ Ref: field.Meta.TagRefSet && field.Meta.TagRef,
Nullable: nullable,
})
}
@@ -953,9 +959,9 @@ func (s *structSerializer) WriteData(ctx *WriteContext,
value reflect.Value) {
baseOffset := buf.WriterIndex()
data := buf.GetData()
- for _, field := range s.fieldGroup.FixedFields {
+ for _, field := range s.fieldGroup.PrimitiveFixedFields {
fieldPtr := unsafe.Add(ptr, field.Offset)
- bufOffset := baseOffset + field.WriteOffset
+ bufOffset := baseOffset + int(field.WriteOffset)
switch field.DispatchId {
case PrimitiveBoolDispatchId:
if *(*bool)(fieldPtr) {
@@ -1080,7 +1086,7 @@ func (s *structSerializer) WriteData(ctx *WriteContext,
value reflect.Value) {
} else if len(s.fieldGroup.FixedFields) > 0 {
// Fallback to reflect-based access for unaddressable values
for _, field := range s.fieldGroup.FixedFields {
- fieldValue := value.Field(field.FieldIndex)
+ fieldValue := value.Field(field.Meta.FieldIndex)
switch field.DispatchId {
// Primitive types (non-pointer)
case PrimitiveBoolDispatchId:
@@ -1134,50 +1140,42 @@ func (s *structSerializer) WriteData(ctx *WriteContext,
value reflect.Value) {
//
==========================================================================
// Phase 2: Varint primitives (int32, int64, int, uint32, uint64, uint,
tagged int64/uint64)
- // - These are variable-length encodings that must be written
sequentially
+ // - Reserve max size once, track offset locally, update writerIndex
once at end
//
==========================================================================
- if canUseUnsafe && len(s.fieldGroup.VarintFields) > 0 {
- for _, field := range s.fieldGroup.VarintFields {
+ if canUseUnsafe && s.fieldGroup.MaxVarintSize > 0 {
+ buf.Reserve(s.fieldGroup.MaxVarintSize)
+ offset := buf.WriterIndex()
+
+ for _, field := range s.fieldGroup.PrimitiveVarintFields {
fieldPtr := unsafe.Add(ptr, field.Offset)
switch field.DispatchId {
case PrimitiveVarint32DispatchId:
- buf.WriteVarint32(*(*int32)(fieldPtr))
- case NotnullVarint32PtrDispatchId:
- buf.WriteVarint32(**(**int32)(fieldPtr))
+ offset += buf.UnsafePutVarInt32(offset,
*(*int32)(fieldPtr))
case PrimitiveVarint64DispatchId:
- buf.WriteVarint64(*(*int64)(fieldPtr))
- case NotnullVarint64PtrDispatchId:
- buf.WriteVarint64(**(**int64)(fieldPtr))
+ offset += buf.UnsafePutVarInt64(offset,
*(*int64)(fieldPtr))
case PrimitiveIntDispatchId:
- buf.WriteVarint64(int64(*(*int)(fieldPtr)))
- case NotnullIntPtrDispatchId:
- buf.WriteVarint64(int64(**(**int)(fieldPtr)))
+ offset += buf.UnsafePutVarInt64(offset,
int64(*(*int)(fieldPtr)))
case PrimitiveVarUint32DispatchId:
- buf.WriteVaruint32(*(*uint32)(fieldPtr))
- case NotnullVarUint32PtrDispatchId:
- buf.WriteVaruint32(**(**uint32)(fieldPtr))
+ offset += buf.UnsafePutVaruint32(offset,
*(*uint32)(fieldPtr))
case PrimitiveVarUint64DispatchId:
- buf.WriteVaruint64(*(*uint64)(fieldPtr))
- case NotnullVarUint64PtrDispatchId:
- buf.WriteVaruint64(**(**uint64)(fieldPtr))
+ offset += buf.UnsafePutVaruint64(offset,
*(*uint64)(fieldPtr))
case PrimitiveUintDispatchId:
- buf.WriteVaruint64(uint64(*(*uint)(fieldPtr)))
- case NotnullUintPtrDispatchId:
- buf.WriteVaruint64(uint64(**(**uint)(fieldPtr)))
+ offset += buf.UnsafePutVaruint64(offset,
uint64(*(*uint)(fieldPtr)))
case PrimitiveTaggedInt64DispatchId:
- buf.WriteTaggedInt64(*(*int64)(fieldPtr))
- case NotnullTaggedInt64PtrDispatchId:
- buf.WriteTaggedInt64(**(**int64)(fieldPtr))
+ offset += buf.UnsafePutTaggedInt64(offset,
*(*int64)(fieldPtr))
case PrimitiveTaggedUint64DispatchId:
- buf.WriteTaggedUint64(*(*uint64)(fieldPtr))
- case NotnullTaggedUint64PtrDispatchId:
- buf.WriteTaggedUint64(**(**uint64)(fieldPtr))
+ offset += buf.UnsafePutTaggedUint64(offset,
*(*uint64)(fieldPtr))
+ default:
+ // Notnull pointer types (rare case - pointers
with nullable=false tag)
+ offset += writeNotnullVarintPtrUnsafe(buf,
offset, fieldPtr, field.DispatchId)
}
}
+ // Update writer index ONCE after all varint fields
+ buf.SetWriterIndex(offset)
} else if len(s.fieldGroup.VarintFields) > 0 {
// Slow path for non-addressable values: use reflection
for _, field := range s.fieldGroup.VarintFields {
- fieldValue := value.Field(field.FieldIndex)
+ fieldValue := value.Field(field.Meta.FieldIndex)
switch field.DispatchId {
// Primitive types (non-pointer)
case PrimitiveVarint32DispatchId:
@@ -1270,7 +1268,7 @@ func (s *structSerializer) writeRemainingField(ctx
*WriteContext, ptr unsafe.Poi
return
case EnumDispatchId:
// Enums don't track refs - always use fast path
- writeEnumField(ctx, field,
value.Field(field.FieldIndex))
+ writeEnumField(ctx, field,
value.Field(field.Meta.FieldIndex))
return
case StringSliceDispatchId:
if field.RefMode == RefModeTracking {
@@ -1541,7 +1539,7 @@ func (s *structSerializer) writeRemainingField(ctx
*WriteContext, ptr unsafe.Poi
}
// Slow path: use reflection for non-addressable values
- fieldValue := value.Field(field.FieldIndex)
+ fieldValue := value.Field(field.Meta.FieldIndex)
// Handle nullable types via reflection when ptr is nil
(non-addressable)
switch field.DispatchId {
@@ -1685,7 +1683,7 @@ func (s *structSerializer) writeRemainingField(ctx
*WriteContext, ptr unsafe.Poi
// Fall back to serializer for other types
if field.Serializer != nil {
- field.Serializer.Write(ctx, field.RefMode, field.WriteType,
field.HasGenerics, fieldValue)
+ field.Serializer.Write(ctx, field.RefMode,
field.Meta.WriteType, field.Meta.HasGenerics, fieldValue)
} else {
ctx.WriteValue(fieldValue, RefModeTracking, true)
}
@@ -1795,9 +1793,9 @@ func (s *structSerializer) ReadData(ctx *ReadContext,
type_ reflect.Type, value
baseOffset := buf.ReaderIndex()
data := buf.GetData()
- for _, field := range s.fieldGroup.FixedFields {
+ for _, field := range s.fieldGroup.PrimitiveFixedFields {
fieldPtr := unsafe.Add(ptr, field.Offset)
- bufOffset := baseOffset + field.WriteOffset
+ bufOffset := baseOffset + int(field.WriteOffset)
switch field.DispatchId {
case PrimitiveBoolDispatchId:
*(*bool)(fieldPtr) = data[bufOffset] != 0
@@ -1938,9 +1936,9 @@ func (s *structSerializer) ReadData(ctx *ReadContext,
type_ reflect.Type, value
// Phase 2: Varint primitives (must read sequentially - variable length)
// Note: For tagged int64/uint64, we can't use unsafe reads because
they need bounds checking
- if len(s.fieldGroup.VarintFields) > 0 {
+ if len(s.fieldGroup.PrimitiveVarintFields) > 0 {
err := ctx.Err()
- for _, field := range s.fieldGroup.VarintFields {
+ for _, field := range s.fieldGroup.PrimitiveVarintFields {
fieldPtr := unsafe.Add(ptr, field.Offset)
switch field.DispatchId {
case PrimitiveVarint32DispatchId:
@@ -2050,7 +2048,7 @@ func (s *structSerializer) readRemainingField(ctx
*ReadContext, ptr unsafe.Point
return
case EnumDispatchId:
// Enums don't track refs - always use fast path
- fieldValue := value.Field(field.FieldIndex)
+ fieldValue := value.Field(field.Meta.FieldIndex)
readEnumField(ctx, field, fieldValue)
return
case StringSliceDispatchId:
@@ -2326,9 +2324,9 @@ func (s *structSerializer) readRemainingField(ctx
*ReadContext, ptr unsafe.Point
}
// Slow path for RefModeTracking cases that break from the switch above
- fieldValue := value.Field(field.FieldIndex)
+ fieldValue := value.Field(field.Meta.FieldIndex)
if field.Serializer != nil {
- field.Serializer.Read(ctx, field.RefMode, field.WriteType,
field.HasGenerics, fieldValue)
+ field.Serializer.Read(ctx, field.RefMode, field.Meta.WriteType,
field.Meta.HasGenerics, fieldValue)
} else {
ctx.ReadValue(fieldValue, RefModeTracking, true)
}
@@ -2343,7 +2341,7 @@ func (s *structSerializer) readFieldsInOrder(ctx
*ReadContext, value reflect.Val
err := ctx.Err()
for i := range s.fields {
field := &s.fields[i]
- if field.FieldIndex < 0 {
+ if field.Meta.FieldIndex < 0 {
s.skipField(ctx, field)
if ctx.HasError() {
return
@@ -2352,7 +2350,7 @@ func (s *structSerializer) readFieldsInOrder(ctx
*ReadContext, value reflect.Val
}
// Fast path for fixed-size primitive types (no ref flag from
remote schema)
- if isFixedSizePrimitive(field.DispatchId, field.Nullable) {
+ if isFixedSizePrimitive(field.DispatchId, field.Meta.Nullable) {
fieldPtr := unsafe.Add(ptr, field.Offset)
switch field.DispatchId {
// PrimitiveXxxDispatchId: local field is non-pointer
type
@@ -2428,7 +2426,7 @@ func (s *structSerializer) readFieldsInOrder(ctx
*ReadContext, value reflect.Val
}
// Fast path for varint primitive types (no ref flag from
remote schema)
- if isVarintPrimitive(field.DispatchId, field.Nullable) &&
!fieldHasNonPrimitiveSerializer(field) {
+ if isVarintPrimitive(field.DispatchId, field.Meta.Nullable) &&
!fieldHasNonPrimitiveSerializer(field) {
fieldPtr := unsafe.Add(ptr, field.Offset)
switch field.DispatchId {
// PrimitiveXxxDispatchId: local field is non-pointer
type
@@ -2486,7 +2484,7 @@ func (s *structSerializer) readFieldsInOrder(ctx
*ReadContext, value reflect.Val
}
// Get field value for nullable primitives and non-primitives
- fieldValue := value.Field(field.FieldIndex)
+ fieldValue := value.Field(field.Meta.FieldIndex)
// Handle nullable fixed-size primitives (read ref flag + fixed
bytes)
// These have Nullable=true but use fixed encoding, not varint
@@ -2657,7 +2655,7 @@ func (s *structSerializer) readFieldsInOrder(ctx
*ReadContext, value reflect.Val
// Slow path for non-primitives (all need ref flag per xlang
spec)
if field.Serializer != nil {
// Use pre-computed RefMode and WriteType from field
initialization
- field.Serializer.Read(ctx, field.RefMode,
field.WriteType, field.HasGenerics, fieldValue)
+ field.Serializer.Read(ctx, field.RefMode,
field.Meta.WriteType, field.Meta.HasGenerics, fieldValue)
} else {
ctx.ReadValue(fieldValue, RefModeTracking, true)
}
@@ -2667,20 +2665,20 @@ func (s *structSerializer) readFieldsInOrder(ctx
*ReadContext, value reflect.Val
// skipField skips a field that doesn't exist or is incompatible
// Uses context error state for deferred error checking.
func (s *structSerializer) skipField(ctx *ReadContext, field *FieldInfo) {
- if field.FieldDef.name != "" {
- fieldDefIsStructType :=
isStructFieldType(field.FieldDef.fieldType)
+ if field.Meta.FieldDef.name != "" {
+ fieldDefIsStructType :=
isStructFieldType(field.Meta.FieldDef.fieldType)
// Use FieldDef's trackingRef and nullable to determine if ref
flag was written by Java
// Java writes ref flag based on its FieldDef, not Go's field
type
- readRefFlag := field.FieldDef.trackingRef ||
field.FieldDef.nullable
- SkipFieldValueWithTypeFlag(ctx, field.FieldDef, readRefFlag,
ctx.Compatible() && fieldDefIsStructType)
+ readRefFlag := field.Meta.FieldDef.trackingRef ||
field.Meta.FieldDef.nullable
+ SkipFieldValueWithTypeFlag(ctx, field.Meta.FieldDef,
readRefFlag, ctx.Compatible() && fieldDefIsStructType)
return
}
// No FieldDef available, read into temp value
- tempValue := reflect.New(field.Type).Elem()
+ tempValue := reflect.New(field.Meta.Type).Elem()
if field.Serializer != nil {
- readType := ctx.Compatible() && isStructField(field.Type)
+ readType := ctx.Compatible() && isStructField(field.Meta.Type)
refMode := RefModeNone
- if field.Nullable {
+ if field.Meta.Nullable {
refMode = RefModeTracking
}
field.Serializer.Read(ctx, refMode, readType, false, tempValue)
@@ -2712,7 +2710,7 @@ func writeEnumField(ctx *WriteContext, field *FieldInfo,
fieldValue reflect.Valu
if fieldValue.IsNil() {
// RefModeNone but nil pointer - this is a protocol
error in schema-consistent mode
// Write zero value as fallback
- targetValue = reflect.Zero(field.Type.Elem())
+ targetValue = reflect.Zero(field.Meta.Type.Elem())
} else {
targetValue = fieldValue.Elem()
}
@@ -2750,7 +2748,7 @@ func readEnumField(ctx *ReadContext, field *FieldInfo,
fieldValue reflect.Value)
// For pointer enum fields, allocate a new value
targetValue := fieldValue
if isPointer {
- newVal := reflect.New(field.Type.Elem())
+ newVal := reflect.New(field.Meta.Type.Elem())
fieldValue.Set(newVal)
targetValue = newVal.Elem()
}
@@ -2758,9 +2756,9 @@ func readEnumField(ctx *ReadContext, field *FieldInfo,
fieldValue reflect.Value)
// For pointer enum fields, the serializer is ptrToValueSerializer
wrapping enumSerializer.
// We need to call the inner enumSerializer directly with the
dereferenced value.
if ptrSer, ok := field.Serializer.(*ptrToValueSerializer); ok {
- ptrSer.valueSerializer.ReadData(ctx, field.Type.Elem(),
targetValue)
+ ptrSer.valueSerializer.ReadData(ctx, field.Meta.Type.Elem(),
targetValue)
} else {
- field.Serializer.ReadData(ctx, field.Type, targetValue)
+ field.Serializer.ReadData(ctx, field.Meta.Type, targetValue)
}
}
diff --git a/go/fory/types.go b/go/fory/types.go
index a8edfc6b9..0da3b7dfc 100644
--- a/go/fory/types.go
+++ b/go/fory/types.go
@@ -279,28 +279,32 @@ type DispatchId uint8
const (
UnknownDispatchId DispatchId = iota
- // Primitive (non-nullable) dispatch IDs - match Java's PRIMITIVE_*
constants
- PrimitiveBoolDispatchId
- PrimitiveInt8DispatchId
- PrimitiveInt16DispatchId
- PrimitiveInt32DispatchId
- PrimitiveVarint32DispatchId
- PrimitiveInt64DispatchId
- PrimitiveVarint64DispatchId
- PrimitiveTaggedInt64DispatchId
- PrimitiveFloat32DispatchId
- PrimitiveFloat64DispatchId
- PrimitiveUint8DispatchId
- PrimitiveUint16DispatchId
- PrimitiveUint32DispatchId
- PrimitiveVarUint32DispatchId
- PrimitiveUint64DispatchId
- PrimitiveVarUint64DispatchId
- PrimitiveTaggedUint64DispatchId
- PrimitiveIntDispatchId // Go-specific: native int
- PrimitiveUintDispatchId // Go-specific: native uint
-
- // Nullable dispatch IDs - match Java's non-PRIMITIVE_* constants
+ // ========== VARINT PRIMITIVES (contiguous for efficient jump table)
==========
+ // These are used in the hot varint serialization loop
+ PrimitiveVarint32DispatchId // 1 - int32 with varint encoding (most
common)
+ PrimitiveVarint64DispatchId // 2 - int64 with varint encoding
+ PrimitiveIntDispatchId // 3 - Go-specific: native int
+ PrimitiveVarUint32DispatchId // 4 - uint32 with varint encoding
+ PrimitiveVarUint64DispatchId // 5 - uint64 with varint encoding
+ PrimitiveUintDispatchId // 6 - Go-specific: native uint
+ PrimitiveTaggedInt64DispatchId // 7 - int64 with tagged encoding
+ PrimitiveTaggedUint64DispatchId // 8 - uint64 with tagged encoding
+
+ // ========== FIXED-SIZE PRIMITIVES (contiguous for efficient jump
table) ==========
+ // These are used in the hot fixed-size serialization loop
+ PrimitiveBoolDispatchId // 9
+ PrimitiveInt8DispatchId // 10
+ PrimitiveUint8DispatchId // 11
+ PrimitiveInt16DispatchId // 12
+ PrimitiveUint16DispatchId // 13
+ PrimitiveInt32DispatchId // 14 - int32 with fixed encoding
+ PrimitiveUint32DispatchId // 15 - uint32 with fixed encoding
+ PrimitiveInt64DispatchId // 16 - int64 with fixed encoding
+ PrimitiveUint64DispatchId // 17 - uint64 with fixed encoding
+ PrimitiveFloat32DispatchId // 18
+ PrimitiveFloat64DispatchId // 19
+
+ // ========== NULLABLE DISPATCH IDs ==========
NullableBoolDispatchId
NullableInt8DispatchId
NullableInt16DispatchId
@@ -321,8 +325,8 @@ const (
NullableIntDispatchId // Go-specific: *int
NullableUintDispatchId // Go-specific: *uint
- // Notnull pointer dispatch IDs - pointer types with nullable=false
- // Write without null flag; on read, create default value if remote
sends null
+ // ========== NOTNULL POINTER DISPATCH IDs ==========
+ // Pointer types with nullable=false - write without null flag
NotnullBoolPtrDispatchId
NotnullInt8PtrDispatchId
NotnullInt16PtrDispatchId
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]