Skip to content

Commit c602466

Browse files
committed
optimize struct fields serialization perf
1 parent 724fece commit c602466

File tree

5 files changed

+297
-187
lines changed

5 files changed

+297
-187
lines changed

go/fory/buffer.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1539,6 +1539,47 @@ func (b *ByteBuffer) UnsafePutInt64(offset int, value int64) int {
15391539
return 8
15401540
}
15411541

1542+
// UnsafePutTaggedInt64 writes int64 using tagged encoding at the given offset.
1543+
// Caller must have ensured capacity (9 bytes max).
1544+
// Returns the number of bytes written (4 or 9).
1545+
//
1546+
//go:inline
1547+
func (b *ByteBuffer) UnsafePutTaggedInt64(offset int, value int64) int {
1548+
const halfMinIntValue int64 = -1073741824 // INT32_MIN / 2
1549+
const halfMaxIntValue int64 = 1073741823 // INT32_MAX / 2
1550+
if value >= halfMinIntValue && value <= halfMaxIntValue {
1551+
binary.LittleEndian.PutUint32(b.data[offset:], uint32(int32(value)<<1))
1552+
return 4
1553+
}
1554+
b.data[offset] = 0b1
1555+
if isLittleEndian {
1556+
*(*int64)(unsafe.Pointer(&b.data[offset+1])) = value
1557+
} else {
1558+
binary.LittleEndian.PutUint64(b.data[offset+1:], uint64(value))
1559+
}
1560+
return 9
1561+
}
1562+
1563+
// UnsafePutTaggedUint64 writes uint64 using tagged encoding at the given offset.
1564+
// Caller must have ensured capacity (9 bytes max).
1565+
// Returns the number of bytes written (4 or 9).
1566+
//
1567+
//go:inline
1568+
func (b *ByteBuffer) UnsafePutTaggedUint64(offset int, value uint64) int {
1569+
const maxSmallValue uint64 = 0x7fffffff // INT32_MAX as u64
1570+
if value <= maxSmallValue {
1571+
binary.LittleEndian.PutUint32(b.data[offset:], uint32(value)<<1)
1572+
return 4
1573+
}
1574+
b.data[offset] = 0b1
1575+
if isLittleEndian {
1576+
*(*uint64)(unsafe.Pointer(&b.data[offset+1])) = value
1577+
} else {
1578+
binary.LittleEndian.PutUint64(b.data[offset+1:], value)
1579+
}
1580+
return 9
1581+
}
1582+
15421583
// ReadVaruint32Small7 reads a varuint32 in small-7 format with error checking
15431584
func (b *ByteBuffer) ReadVaruint32Small7(err *Error) uint32 {
15441585
if b.readerIndex >= len(b.data) {

go/fory/field_info.go

Lines changed: 76 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -24,27 +24,31 @@ import (
2424
"strings"
2525
)
2626

27-
// FieldInfo stores field metadata computed ENTIRELY at init time.
28-
// All flags and decisions are pre-computed to eliminate runtime checks.
29-
type FieldInfo struct {
27+
// PrimitiveFieldInfo contains only the fields needed for hot primitive serialization loops.
28+
// This minimal struct improves cache efficiency during iteration.
29+
// Size: 16 bytes (vs full FieldInfo)
30+
type PrimitiveFieldInfo struct {
31+
Offset uintptr // Field offset for unsafe access
32+
DispatchId DispatchId // Type dispatch ID
33+
WriteOffset uint8 // Offset within fixed-fields buffer (0-255, sufficient for fixed primitives)
34+
}
35+
36+
// FieldMeta contains cold/rarely-accessed field metadata.
37+
// Accessed via pointer from FieldInfo to keep FieldInfo small for cache efficiency.
38+
type FieldMeta struct {
3039
Name string
31-
Offset uintptr
3240
Type reflect.Type
33-
DispatchId DispatchId
34-
TypeId TypeId // Fory type ID for the serializer
35-
Serializer Serializer
41+
TypeId TypeId // Fory type ID for the serializer
3642
Nullable bool
3743
FieldIndex int // -1 if field doesn't exist in current struct (for compatible mode)
3844
FieldDef FieldDef // original FieldDef from remote TypeDef (for compatible mode skip)
3945

40-
// Pre-computed sizes and offsets (for fixed primitives)
41-
FixedSize int // 0 if not fixed-size, else 1/2/4/8
42-
WriteOffset int // Offset within fixed-fields buffer region (sum of preceding field sizes)
46+
// Pre-computed sizes (for fixed primitives)
47+
FixedSize int // 0 if not fixed-size, else 1/2/4/8
4348

4449
// Pre-computed flags for serialization (computed at init time)
45-
RefMode RefMode // ref mode for serializer.Write/Read
46-
WriteType bool // whether to write type info (true for struct fields in compatible mode)
47-
HasGenerics bool // whether element types are known from TypeDef (for container fields)
50+
WriteType bool // whether to write type info (true for struct fields in compatible mode)
51+
HasGenerics bool // whether element types are known from TypeDef (for container fields)
4852

4953
// Tag-based configuration (from fory struct tags)
5054
TagID int // -1 = use field name, >=0 = use tag ID
@@ -53,9 +57,21 @@ type FieldInfo struct {
5357
TagRef bool // The ref value from fory tag (only valid if TagRefSet is true)
5458
TagNullableSet bool // Whether nullable was explicitly set via fory tag
5559
TagNullable bool // The nullable value from fory tag (only valid if TagNullableSet is true)
60+
}
5661

57-
// Pre-computed type flags (computed at init time to avoid runtime reflection)
58-
IsPtr bool // True if field.Type.Kind() == reflect.Ptr
62+
// FieldInfo stores field metadata computed ENTIRELY at init time.
63+
// Hot fields are kept inline for cache efficiency, cold fields accessed via Meta pointer.
64+
type FieldInfo struct {
65+
// Hot fields - accessed frequently during serialization
66+
Offset uintptr // Field offset for unsafe access
67+
DispatchId DispatchId // Type dispatch ID
68+
WriteOffset int // Offset within fixed-fields buffer region (sum of preceding field sizes)
69+
RefMode RefMode // ref mode for serializer.Write/Read
70+
IsPtr bool // True if field.Type.Kind() == reflect.Ptr
71+
Serializer Serializer // Serializer for this field
72+
73+
// Cold fields - accessed less frequently
74+
Meta *FieldMeta
5975
}
6076

6177
// FieldGroup holds categorized and sorted fields for optimized serialization.
@@ -65,6 +81,11 @@ type FieldInfo struct {
6581
// - VarintFields: non-nullable varint primitives (varint32/64, var_uint32/64, tagged_int64/uint64)
6682
// - RemainingFields: all other fields (nullable primitives, strings, collections, structs, etc.)
6783
type FieldGroup struct {
84+
// Primitive field slices - minimal data for fast iteration in hot loops
85+
PrimitiveFixedFields []PrimitiveFieldInfo // Minimal fixed field info for hot loop
86+
PrimitiveVarintFields []PrimitiveFieldInfo // Minimal varint field info for hot loop
87+
88+
// Full field info for remaining fields and fallback paths
6889
FixedFields []FieldInfo // Non-nullable fixed-size primitives
6990
VarintFields []FieldInfo // Non-nullable varint primitives
7091
RemainingFields []FieldInfo // All other fields
@@ -100,19 +121,19 @@ func (g *FieldGroup) DebugPrint(typeName string) {
100121
for i := range g.FixedFields {
101122
f := &g.FixedFields[i]
102123
fmt.Printf("[Go] [%d] %s -> dispatchId=%d, typeId=%d, size=%d, nullable=%v\n",
103-
i, f.Name, f.DispatchId, f.TypeId, f.FixedSize, f.Nullable)
124+
i, f.Meta.Name, f.DispatchId, f.Meta.TypeId, f.Meta.FixedSize, f.Meta.Nullable)
104125
}
105126
fmt.Printf("[Go] Go sorted varintFields (%d):\n", len(g.VarintFields))
106127
for i := range g.VarintFields {
107128
f := &g.VarintFields[i]
108129
fmt.Printf("[Go] [%d] %s -> dispatchId=%d, typeId=%d, nullable=%v\n",
109-
i, f.Name, f.DispatchId, f.TypeId, f.Nullable)
130+
i, f.Meta.Name, f.DispatchId, f.Meta.TypeId, f.Meta.Nullable)
110131
}
111132
fmt.Printf("[Go] Go sorted remainingFields (%d):\n", len(g.RemainingFields))
112133
for i := range g.RemainingFields {
113134
f := &g.RemainingFields[i]
114135
fmt.Printf("[Go] [%d] %s -> dispatchId=%d, typeId=%d, nullable=%v\n",
115-
i, f.Name, f.DispatchId, f.TypeId, f.Nullable)
136+
i, f.Meta.Name, f.DispatchId, f.Meta.TypeId, f.Meta.Nullable)
116137
}
117138
fmt.Printf("[Go] ===========================================\n")
118139
}
@@ -126,11 +147,11 @@ func GroupFields(fields []FieldInfo) FieldGroup {
126147
// Categorize fields
127148
for i := range fields {
128149
field := &fields[i]
129-
if isFixedSizePrimitive(field.DispatchId, field.Nullable) {
150+
if isFixedSizePrimitive(field.DispatchId, field.Meta.Nullable) {
130151
// Non-nullable fixed-size primitives only
131-
field.FixedSize = getFixedSizeByDispatchId(field.DispatchId)
152+
field.Meta.FixedSize = getFixedSizeByDispatchId(field.DispatchId)
132153
g.FixedFields = append(g.FixedFields, *field)
133-
} else if isVarintPrimitive(field.DispatchId, field.Nullable) {
154+
} else if isVarintPrimitive(field.DispatchId, field.Meta.Nullable) {
134155
// Non-nullable varint primitives only
135156
g.VarintFields = append(g.VarintFields, *field)
136157
} else {
@@ -142,19 +163,25 @@ func GroupFields(fields []FieldInfo) FieldGroup {
142163
// Sort fixedFields: size desc, typeId desc, name asc
143164
sort.SliceStable(g.FixedFields, func(i, j int) bool {
144165
fi, fj := &g.FixedFields[i], &g.FixedFields[j]
145-
if fi.FixedSize != fj.FixedSize {
146-
return fi.FixedSize > fj.FixedSize // size descending
166+
if fi.Meta.FixedSize != fj.Meta.FixedSize {
167+
return fi.Meta.FixedSize > fj.Meta.FixedSize // size descending
147168
}
148-
if fi.TypeId != fj.TypeId {
149-
return fi.TypeId > fj.TypeId // typeId descending
169+
if fi.Meta.TypeId != fj.Meta.TypeId {
170+
return fi.Meta.TypeId > fj.Meta.TypeId // typeId descending
150171
}
151-
return fi.Name < fj.Name // name ascending
172+
return fi.Meta.Name < fj.Meta.Name // name ascending
152173
})
153174

154-
// Compute WriteOffset after sorting
175+
// Compute WriteOffset after sorting and build primitive field slice
176+
g.PrimitiveFixedFields = make([]PrimitiveFieldInfo, len(g.FixedFields))
155177
for i := range g.FixedFields {
156178
g.FixedFields[i].WriteOffset = g.FixedSize
157-
g.FixedSize += g.FixedFields[i].FixedSize
179+
g.PrimitiveFixedFields[i] = PrimitiveFieldInfo{
180+
Offset: g.FixedFields[i].Offset,
181+
DispatchId: g.FixedFields[i].DispatchId,
182+
WriteOffset: uint8(g.FixedSize),
183+
}
184+
g.FixedSize += g.FixedFields[i].Meta.FixedSize
158185
}
159186

160187
// Sort varintFields: underlying type size desc, typeId desc, name asc
@@ -166,15 +193,21 @@ func GroupFields(fields []FieldInfo) FieldGroup {
166193
if sizeI != sizeJ {
167194
return sizeI > sizeJ // size descending
168195
}
169-
if fi.TypeId != fj.TypeId {
170-
return fi.TypeId > fj.TypeId // typeId descending
196+
if fi.Meta.TypeId != fj.Meta.TypeId {
197+
return fi.Meta.TypeId > fj.Meta.TypeId // typeId descending
171198
}
172-
return fi.Name < fj.Name // name ascending
199+
return fi.Meta.Name < fj.Meta.Name // name ascending
173200
})
174201

175-
// Compute maxVarintSize
202+
// Compute maxVarintSize and build primitive varint field slice
203+
g.PrimitiveVarintFields = make([]PrimitiveFieldInfo, len(g.VarintFields))
176204
for i := range g.VarintFields {
177205
g.MaxVarintSize += getVarintMaxSizeByDispatchId(g.VarintFields[i].DispatchId)
206+
g.PrimitiveVarintFields[i] = PrimitiveFieldInfo{
207+
Offset: g.VarintFields[i].Offset,
208+
DispatchId: g.VarintFields[i].DispatchId,
209+
// WriteOffset not used for varint fields (variable length)
210+
}
178211
}
179212

180213
// Sort remainingFields: nullable primitives first (by primitiveComparator),
@@ -192,8 +225,8 @@ func GroupFields(fields []FieldInfo) FieldGroup {
192225
// Within other internal types category (STRING, BINARY, LIST, SET, MAP),
193226
// sort by typeId then by sort key (tagID if available, otherwise name).
194227
if catI == 1 {
195-
if fi.TypeId != fj.TypeId {
196-
return fi.TypeId < fj.TypeId
228+
if fi.Meta.TypeId != fj.Meta.TypeId {
229+
return fi.Meta.TypeId < fj.Meta.TypeId
197230
}
198231
return getFieldSortKey(fi) < getFieldSortKey(fj)
199232
}
@@ -215,7 +248,7 @@ func fieldHasNonPrimitiveSerializer(field *FieldInfo) bool {
215248
// all require special serialization and should not use the primitive fast path
216249
// Note: ENUM uses unsigned Varuint32Small7 for ordinals, not signed zigzag varint
217250
// Use internal type ID (low 8 bits) since registered types have composite TypeIds like (userID << 8) | internalID
218-
internalTypeId := TypeId(field.TypeId & 0xFF)
251+
internalTypeId := TypeId(field.Meta.TypeId & 0xFF)
219252
switch internalTypeId {
220253
case ENUM, NAMED_ENUM, NAMED_STRUCT, NAMED_COMPATIBLE_STRUCT, NAMED_EXT:
221254
return true
@@ -229,7 +262,7 @@ func isEnumField(field *FieldInfo) bool {
229262
if field.Serializer == nil {
230263
return false
231264
}
232-
internalTypeId := field.TypeId & 0xFF
265+
internalTypeId := field.Meta.TypeId & 0xFF
233266
return internalTypeId == ENUM || internalTypeId == NAMED_ENUM
234267
}
235268

@@ -241,7 +274,7 @@ func getFieldCategory(field *FieldInfo) int {
241274
if isNullableFixedSizePrimitive(field.DispatchId) || isNullableVarintPrimitive(field.DispatchId) {
242275
return 0
243276
}
244-
internalId := field.TypeId & 0xFF
277+
internalId := field.Meta.TypeId & 0xFF
245278
switch TypeId(internalId) {
246279
case STRING, BINARY, LIST, SET, MAP:
247280
// Internal types: sorted by typeId, then name
@@ -267,10 +300,10 @@ func comparePrimitiveFields(fi, fj *FieldInfo) bool {
267300
if sizeI != sizeJ {
268301
return sizeI > sizeJ // size descending
269302
}
270-
if fi.TypeId != fj.TypeId {
271-
return fi.TypeId > fj.TypeId // typeId descending
303+
if fi.Meta.TypeId != fj.Meta.TypeId {
304+
return fi.Meta.TypeId > fj.Meta.TypeId // typeId descending
272305
}
273-
return fi.Name < fj.Name // name ascending
306+
return fi.Meta.Name < fj.Meta.Name // name ascending
274307
}
275308

276309
// getNullableFixedSize returns the fixed size for nullable fixed primitives
@@ -532,10 +565,10 @@ func (t triple) getSortKey() string {
532565
// If TagID >= 0, returns the tag ID as string (for tag-based sorting).
533566
// Otherwise returns the field name (which is already snake_case).
534567
func getFieldSortKey(f *FieldInfo) string {
535-
if f.TagID >= 0 {
536-
return fmt.Sprintf("%d", f.TagID)
568+
if f.Meta.TagID >= 0 {
569+
return fmt.Sprintf("%d", f.Meta.TagID)
537570
}
538-
return f.Name
571+
return f.Meta.Name
539572
}
540573

541574
// sortFields sorts fields with nullable information to match Java's field ordering.

go/fory/primitive.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package fory
1919

2020
import (
2121
"reflect"
22+
"unsafe"
2223
)
2324

2425
// ============================================================================
@@ -563,3 +564,36 @@ func (s float64Serializer) Read(ctx *ReadContext, refMode RefMode, readType bool
563564
func (s float64Serializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) {
564565
s.Read(ctx, refMode, false, false, value)
565566
}
567+
568+
// ============================================================================
569+
// Notnull Pointer Helper Functions for Varint Types
570+
// These are used by struct serializer for the rare case of *T with nullable=false
571+
// ============================================================================
572+
573+
// writeNotnullVarintPtrUnsafe writes a notnull pointer varint type at the given offset.
574+
// Used by struct serializer for rare notnull pointer types.
575+
// Returns the number of bytes written.
576+
//
577+
//go:inline
578+
func writeNotnullVarintPtrUnsafe(buf *ByteBuffer, offset int, fieldPtr unsafe.Pointer, dispatchId DispatchId) int {
579+
switch dispatchId {
580+
case NotnullVarint32PtrDispatchId:
581+
return buf.UnsafePutVarInt32(offset, **(**int32)(fieldPtr))
582+
case NotnullVarint64PtrDispatchId:
583+
return buf.UnsafePutVarInt64(offset, **(**int64)(fieldPtr))
584+
case NotnullIntPtrDispatchId:
585+
return buf.UnsafePutVarInt64(offset, int64(**(**int)(fieldPtr)))
586+
case NotnullVarUint32PtrDispatchId:
587+
return buf.UnsafePutVaruint32(offset, **(**uint32)(fieldPtr))
588+
case NotnullVarUint64PtrDispatchId:
589+
return buf.UnsafePutVaruint64(offset, **(**uint64)(fieldPtr))
590+
case NotnullUintPtrDispatchId:
591+
return buf.UnsafePutVaruint64(offset, uint64(**(**uint)(fieldPtr)))
592+
case NotnullTaggedInt64PtrDispatchId:
593+
return buf.UnsafePutTaggedInt64(offset, **(**int64)(fieldPtr))
594+
case NotnullTaggedUint64PtrDispatchId:
595+
return buf.UnsafePutTaggedUint64(offset, **(**uint64)(fieldPtr))
596+
default:
597+
return 0
598+
}
599+
}

0 commit comments

Comments
 (0)