diff --git a/go/fory/buffer.go b/go/fory/buffer.go index 13ec17f4af..74e4498d75 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 cac8ae4465..7739894eca 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 b0bf967677..d8978b88ac 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 b8f3c77658..e38f51a4ef 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 a8edfc6b94..0da3b7dfc0 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