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 b7c16d0f0 perf(dart): optimize struct deserialize with direct dispatch
codegen (#3563)
b7c16d0f0 is described below
commit b7c16d0f04c743b910407f49895d5edddc6a060d
Author: Yash Agarwal <[email protected]>
AuthorDate: Tue Apr 14 17:36:41 2026 +0530
perf(dart): optimize struct deserialize with direct dispatch codegen (#3563)
## Why?
Fory struct deserialize in Dart was slower than it should be given
Fory's wire format.
## What does this PR do?
- Codegen emits direct read calls for statically safe struct/ext fields,
skipping runtime
flag and type switches (mirrors Java's ObjectCodecBuilder).
- Runtime replaces a one-key HashMap on ReadContext/WriteContext with a
dedicated field.
- Fixes benchmark workload file broken by the #3560 constructor
refactor.
## Related issues
#3558
## AI Contribution Checklist
- [ ] Substantial AI assistance was used in this PR: `yes` / `no`
- [ ] If `yes`, I included a completed [AI Contribution
Checklist](https://github.com/apache/fory/blob/main/AI_POLICY.md#9-contributor-checklist-for-ai-assisted-prs)
in this PR description and the required `AI Usage Disclosure`.
- [ ] If `yes`, my PR description includes the required `ai_review`
summary and screenshot evidence of the final clean AI review results
from both fresh reviewers on the current PR diff or current HEAD after
the latest code changes.
## 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
Co-authored-by: Shawn Yang <[email protected]>
---
benchmarks/dart/lib/src/workloads.dart | 2 +-
.../fory/lib/src/codegen/fory_generator.dart | 90 +++++++++++++++++-----
.../fory/lib/src/codegen/generated_support.dart | 42 ++++++++--
.../fory/lib/src/context/read_context.dart | 33 ++------
.../fory/lib/src/context/write_context.dart | 34 ++------
.../fory/lib/src/serializer/struct_serializer.dart | 40 ++++------
.../fory/lib/src/serializer/struct_slots.dart | 6 --
7 files changed, 133 insertions(+), 114 deletions(-)
diff --git a/benchmarks/dart/lib/src/workloads.dart
b/benchmarks/dart/lib/src/workloads.dart
index 12766c37d..32f598872 100644
--- a/benchmarks/dart/lib/src/workloads.dart
+++ b/benchmarks/dart/lib/src/workloads.dart
@@ -158,7 +158,7 @@ buildBenchmarkDefinitions() {
}
Fory newBenchmarkFory() {
- final fory = Fory(config: const Config(compatible: true));
+ final fory = Fory(compatible: true);
registerBenchmarkTypes(fory);
return fory;
}
diff --git a/dart/packages/fory/lib/src/codegen/fory_generator.dart
b/dart/packages/fory/lib/src/codegen/fory_generator.dart
index 424301df1..563279041 100644
--- a/dart/packages/fory/lib/src/codegen/fory_generator.dart
+++ b/dart/packages/fory/lib/src/codegen/fory_generator.dart
@@ -501,6 +501,14 @@ final class ForyGenerator extends Generator {
output.writeln(
' value.${field.name} =
${_directGeneratedTypedContainerReadExpression(structSpec.name, field,
'fields[$index]')};',
);
+ } else if (_usesDirectGeneratedStructFieldFastPath(field)) {
+ output.writeln(
+ ' value.${field.name} =
$readerFunctionName(readGeneratedStructDirectValue(context, fields[$index]),
value.${field.name});',
+ );
+ } else if (_usesDirectGeneratedDeclaredReadFastPath(field)) {
+ output.writeln(
+ ' value.${field.name} =
$readerFunctionName(readGeneratedStructDeclaredValue(context, fields[$index]),
value.${field.name});',
+ );
} else {
output.writeln(
' value.${field.name} =
$readerFunctionName(readGeneratedStructFieldInfoValue(context, fields[$index],
value.${field.name}), value.${field.name});',
@@ -531,12 +539,6 @@ final class ForyGenerator extends Generator {
output.writeln(' return value;');
case _ConstructorMode.constructor:
output.writeln(' final slots = generatedStructReadSlots(context);');
- for (var index = 0; index < structSpec.fields.length; index += 1) {
- final field = structSpec.fields[index];
- output.writeln(
- ' late final ${field.displayType} ${field.localName};',
- );
- }
output.writeln(' if (slots == null) {');
if (hasDirectPrimitiveFastPath || directCursorRuns.isNotEmpty) {
output.writeln(' final buffer = context.buffer;');
@@ -555,19 +557,27 @@ final class ForyGenerator extends Generator {
}
if (_usesReservedGeneratedFastPath(field)) {
output.writeln(
- ' ${field.localName} =
${_directGeneratedCursorReadExpression(field,
'cursor${directCursorStartByIndex[index]}')};',
+ ' final ${field.displayType} ${field.localName} =
${_directGeneratedCursorReadExpression(field,
'cursor${directCursorStartByIndex[index]}')};',
);
} else if (_usesDirectGeneratedBasicFastPath(field)) {
output.writeln(
- ' ${field.localName} =
${_directGeneratedReadExpression(field)};',
+ ' final ${field.displayType} ${field.localName} =
${_directGeneratedReadExpression(field)};',
);
} else if (_usesDirectGeneratedTypedContainerReadFastPath(field)) {
output.writeln(
- ' ${field.localName} =
${_directGeneratedTypedContainerReadExpression(structSpec.name, field,
'fields[$index]')};',
+ ' final ${field.displayType} ${field.localName} =
${_directGeneratedTypedContainerReadExpression(structSpec.name, field,
'fields[$index]')};',
+ );
+ } else if (_usesDirectGeneratedStructFieldFastPath(field)) {
+ output.writeln(
+ ' final ${field.displayType} ${field.localName} =
$readerFunctionName(readGeneratedStructDirectValue(context, fields[$index]));',
+ );
+ } else if (_usesDirectGeneratedDeclaredReadFastPath(field)) {
+ output.writeln(
+ ' final ${field.displayType} ${field.localName} =
$readerFunctionName(readGeneratedStructDeclaredValue(context,
fields[$index]));',
);
} else {
output.writeln(
- ' ${field.localName} =
$readerFunctionName(readGeneratedStructFieldInfoValue(context,
fields[$index]));',
+ ' final ${field.displayType} ${field.localName} =
$readerFunctionName(readGeneratedStructFieldInfoValue(context,
fields[$index]));',
);
}
final directCursorEndRun = directCursorRunByEnd[index];
@@ -575,28 +585,45 @@ final class ForyGenerator extends Generator {
output.writeln('
cursor${directCursorEndRun.start}.finish();');
}
}
- output.writeln(' } else {');
+ final constructorInvocation = _constructorInvocation(structSpec);
+ output
+ ..writeln(' final value = $constructorInvocation;')
+ ..writeln(' context.reference(value);');
+ for (final fieldName
+ in structSpec.constructorPlan.postConstructionFieldNames) {
+ final field =
+ structSpec.fields.firstWhere((item) => item.name == fieldName);
+ output.writeln(' value.${field.name} = ${field.localName};');
+ }
+ output.writeln(' return value;');
+ // Slow path: schema-evolution slots present. Use `late final` so each
+ // field can be conditionally assigned from its slot or read fresh.
+ output.writeln(' }');
+ for (var index = 0; index < structSpec.fields.length; index += 1) {
+ final field = structSpec.fields[index];
+ output.writeln(
+ ' late final ${field.displayType} ${field.localName};',
+ );
+ }
for (var index = 0; index < structSpec.fields.length; index += 1) {
final field = structSpec.fields[index];
final readerFunctionName = field.readerFunctionName(structSpec.name);
final rawValueName = 'raw${structSpec.name}$index';
output.writeln(
- ' if (slots.containsSlot($index)) {',
+ ' if (slots.containsSlot($index)) {',
);
output.writeln(
- ' final $rawValueName = slots.valueForSlot($index);',
+ ' final $rawValueName = slots.valueForSlot($index);',
);
output.writeln(
- ' ${field.localName} =
$readerFunctionName(${_slotResolvedRawExpression(rawValueName)});',
+ ' ${field.localName} =
$readerFunctionName(${_slotResolvedRawExpression(rawValueName)});',
);
- output.writeln(' } else {');
+ output.writeln(' } else {');
output.writeln(
- ' ${field.localName} = $readerFunctionName(null);',
+ ' ${field.localName} = $readerFunctionName(null);',
);
- output.writeln(' }');
+ output.writeln(' }');
}
- output.writeln(' }');
- final constructorInvocation = _constructorInvocation(structSpec);
output
..writeln(' final value = $constructorInvocation;')
..writeln(' context.reference(value);');
@@ -939,6 +966,31 @@ GeneratedFieldType(
field.fieldType.typeId == TypeIds.enumById;
}
+ bool _usesDirectGeneratedDeclaredReadFastPath(
+ _GeneratedFieldSpec field,
+ ) {
+ if (field.fieldType.nullable ||
+ field.fieldType.ref ||
+ field.fieldType.dynamic == true) {
+ return false;
+ }
+ final typeId = field.fieldType.typeId;
+ return typeId == TypeIds.ext || typeId == TypeIds.namedExt;
+ }
+
+ bool _usesDirectGeneratedStructFieldFastPath(_GeneratedFieldSpec field) {
+ if (field.fieldType.nullable ||
+ field.fieldType.ref ||
+ field.fieldType.dynamic == true) {
+ return false;
+ }
+ final typeId = field.fieldType.typeId;
+ return typeId == TypeIds.struct ||
+ typeId == TypeIds.compatibleStruct ||
+ typeId == TypeIds.namedStruct ||
+ typeId == TypeIds.namedCompatibleStruct;
+ }
+
bool _usesDirectGeneratedTypedContainerReadFastPath(
_GeneratedFieldSpec field,
) {
diff --git a/dart/packages/fory/lib/src/codegen/generated_support.dart
b/dart/packages/fory/lib/src/codegen/generated_support.dart
index ddaac6335..44ba64105 100644
--- a/dart/packages/fory/lib/src/codegen/generated_support.dart
+++ b/dart/packages/fory/lib/src/codegen/generated_support.dart
@@ -559,16 +559,12 @@ void registerGeneratedStruct<T>(
@internal
StructWriteSlots? generatedStructWriteSlots(WriteContext context) {
- return context.getContextObject<StructWriteSlots>(
- structWriteSlotsKey,
- );
+ return context.structWriteSlots;
}
@internal
StructReadSlots? generatedStructReadSlots(ReadContext context) {
- return context.getContextObject<StructReadSlots>(
- structReadSlotsKey,
- );
+ return context.structReadSlots;
}
@internal
@@ -687,6 +683,40 @@ Object? readGeneratedStructFieldInfoValue(
return readFieldValue(context, field, fallback);
}
+@internal
+Object? readGeneratedStructDeclaredValue(
+ ReadContext context,
+ GeneratedStructFieldInfo field,
+) {
+ final resolved = fieldDeclaredTypeInfo(context.typeResolver, field)!;
+ if (fieldUsesDeclaredType(context.typeResolver, field)) {
+ return context.readResolvedValue(resolved, field.fieldType);
+ }
+ final actualResolved = context.readTypeMetaValue(
+ resolved.isNamed ? resolved : null,
+ );
+ return context.readResolvedValue(actualResolved, field.fieldType);
+}
+
+@internal
+Object readGeneratedStructDirectValue(
+ ReadContext context,
+ GeneratedStructFieldInfo field,
+) {
+ final declared = fieldDeclaredTypeInfo(context.typeResolver, field)!;
+ final resolver.TypeInfo resolved;
+ if (fieldUsesDeclaredType(context.typeResolver, field)) {
+ resolved = declared;
+ } else {
+ resolved =
+ context.readTypeMetaValue(declared.isNamed ? declared : null);
+ }
+ context.increaseDepth();
+ final value = resolved.structSerializer!.readValue(context, resolved);
+ context.decreaseDepth();
+ return value;
+}
+
@internal
List<T> readGeneratedDirectListValue<T>(
ReadContext context,
diff --git a/dart/packages/fory/lib/src/context/read_context.dart
b/dart/packages/fory/lib/src/context/read_context.dart
index ec78c2f7d..f70a79f4e 100644
--- a/dart/packages/fory/lib/src/context/read_context.dart
+++ b/dart/packages/fory/lib/src/context/read_context.dart
@@ -12,6 +12,7 @@ import 'package:fory/src/serializer/map_serializers.dart';
import 'package:fory/src/serializer/primitive_serializers.dart';
import 'package:fory/src/serializer/scalar_serializers.dart';
import 'package:fory/src/serializer/serializer.dart';
+import 'package:fory/src/serializer/struct_slots.dart';
import 'package:fory/src/serializer/typed_array_serializers.dart';
import 'package:fory/src/types/float16.dart';
@@ -29,7 +30,7 @@ final class ReadContext {
late Buffer _buffer;
final List<TypeInfo> _sharedTypes = <TypeInfo>[];
- final Map<Object, Object?> _contextObjects = <Object, Object?>{};
+ StructReadSlots? _structReadSlots;
int _depth = 0;
@internal
@@ -50,7 +51,7 @@ final class ReadContext {
_sharedTypes.clear();
_refReader.reset();
_metaStringReader.reset();
- _contextObjects.clear();
+ _structReadSlots = null;
_depth = 0;
}
@@ -64,33 +65,11 @@ final class ReadContext {
RefReader get refReader => _refReader;
@internal
- bool hasContextObject(Object key) {
- return _contextObjects.containsKey(key);
- }
-
- @internal
- T? getContextObject<T>(Object key) {
- return _contextObjects[key] as T?;
- }
+ StructReadSlots? get structReadSlots => _structReadSlots;
@internal
- Object? replaceContextObject(Object key, Object? next) {
- final previous = _contextObjects[key];
- if (next == null) {
- _contextObjects.remove(key);
- } else {
- _contextObjects[key] = next;
- }
- return previous;
- }
-
- @internal
- void restoreContextObject(Object key, Object? previous) {
- if (previous == null) {
- _contextObjects.remove(key);
- } else {
- _contextObjects[key] = previous;
- }
+ set structReadSlots(StructReadSlots? value) {
+ _structReadSlots = value;
}
@internal
diff --git a/dart/packages/fory/lib/src/context/write_context.dart
b/dart/packages/fory/lib/src/context/write_context.dart
index f039e2f42..d803f87c9 100644
--- a/dart/packages/fory/lib/src/context/write_context.dart
+++ b/dart/packages/fory/lib/src/context/write_context.dart
@@ -15,6 +15,7 @@ import
'package:fory/src/serializer/collection_serializers.dart';
import 'package:fory/src/serializer/map_serializers.dart';
import 'package:fory/src/serializer/primitive_serializers.dart';
import 'package:fory/src/serializer/scalar_serializers.dart';
+import 'package:fory/src/serializer/struct_slots.dart';
import 'package:fory/src/serializer/typed_array_serializers.dart';
import 'package:fory/src/types/float16.dart';
import 'package:fory/src/types/local_date.dart';
@@ -35,8 +36,7 @@ final class WriteContext {
late Buffer _buffer;
final LinkedHashMap<TypeDef, int> _typeDefIds =
LinkedHashMap<TypeDef, int>.identity();
- final LinkedHashMap<Object, Object?> _contextObjects =
- LinkedHashMap<Object, Object?>.identity();
+ StructWriteSlots? _structWriteSlots;
bool _rootTrackRef = false;
int _depth = 0;
@@ -59,7 +59,7 @@ final class WriteContext {
_typeDefIds.clear();
_refWriter.reset();
_metaStringWriter.reset();
- _contextObjects.clear();
+ _structWriteSlots = null;
_rootTrackRef = false;
_depth = 0;
}
@@ -77,33 +77,11 @@ final class WriteContext {
bool get rootTrackRef => _rootTrackRef;
@internal
- bool hasContextObject(Object key) {
- return _contextObjects.containsKey(key);
- }
+ StructWriteSlots? get structWriteSlots => _structWriteSlots;
@internal
- T? getContextObject<T>(Object key) {
- return _contextObjects[key] as T?;
- }
-
- @internal
- Object? replaceContextObject(Object key, Object? next) {
- final previous = _contextObjects[key];
- if (next == null) {
- _contextObjects.remove(key);
- } else {
- _contextObjects[key] = next;
- }
- return previous;
- }
-
- @internal
- void restoreContextObject(Object key, Object? previous) {
- if (previous == null) {
- _contextObjects.remove(key);
- } else {
- _contextObjects[key] = previous;
- }
+ set structWriteSlots(StructWriteSlots? value) {
+ _structWriteSlots = value;
}
/// Records entry into one more nested write frame.
diff --git a/dart/packages/fory/lib/src/serializer/struct_serializer.dart
b/dart/packages/fory/lib/src/serializer/struct_serializer.dart
index 4b5946606..6c7cdde37 100644
--- a/dart/packages/fory/lib/src/serializer/struct_serializer.dart
+++ b/dart/packages/fory/lib/src/serializer/struct_serializer.dart
@@ -92,10 +92,7 @@ final class StructSerializer extends Serializer<Object?> {
final previousCompatibleFields =
_replaceWriteSlots(internal, resolved, value);
_payloadSerializer.write(context, value);
- internal.restoreContextObject(
- structWriteSlotsKey,
- previousCompatibleFields,
- );
+ internal.structWriteSlots = previousCompatibleFields;
}
Object readValue(
@@ -122,35 +119,29 @@ final class StructSerializer extends Serializer<Object?> {
hasCurrentPreservedRef: hasCurrentPreservedRef,
);
}
- final previousReadSlots = internal.replaceContextObject(
- structReadSlotsKey,
- null,
- );
+ final previousReadSlots = internal.structReadSlots;
+ internal.structReadSlots = null;
final value = internal.readSerializerPayload(
_payloadSerializer,
resolved,
hasCurrentPreservedRef: hasCurrentPreservedRef,
);
- internal.restoreContextObject(
- structReadSlotsKey,
- previousReadSlots,
- );
+ internal.structReadSlots = previousReadSlots;
_rememberRemoteMetadata(internal, resolved, value);
return value;
}
- Object? _replaceWriteSlots(
+ StructWriteSlots? _replaceWriteSlots(
WriteContext context,
TypeInfo resolved,
Object value,
) {
final layout = _compatibleWriteLayoutForValue(context, resolved, value);
- return context.replaceContextObject(
- structWriteSlotsKey,
- layout == null
- ? null
- : StructWriteSlots(layout.fields, _localFields.length),
- );
+ final previous = context.structWriteSlots;
+ context.structWriteSlots = layout == null
+ ? null
+ : StructWriteSlots(layout.fields, _localFields.length);
+ return previous;
}
_CompatibleWriteLayout? _compatibleWriteLayoutForValue(
@@ -256,19 +247,14 @@ final class StructSerializer extends Serializer<Object?> {
);
presentSlots[slot] = true;
}
- final previousCompatibleFields = context.replaceContextObject(
- structReadSlotsKey,
- StructReadSlots(compatibleValues, presentSlots),
- );
+ final previousCompatibleFields = context.structReadSlots;
+ context.structReadSlots = StructReadSlots(compatibleValues, presentSlots);
final value = context.readSerializerPayload(
_payloadSerializer,
resolved,
hasCurrentPreservedRef: hasCurrentPreservedRef,
);
- context.restoreContextObject(
- structReadSlotsKey,
- previousCompatibleFields,
- );
+ context.structReadSlots = previousCompatibleFields;
_rememberRemoteMetadata(context, resolved, value);
return value;
}
diff --git a/dart/packages/fory/lib/src/serializer/struct_slots.dart
b/dart/packages/fory/lib/src/serializer/struct_slots.dart
index 73b59f610..8bd2dd56d 100644
--- a/dart/packages/fory/lib/src/serializer/struct_slots.dart
+++ b/dart/packages/fory/lib/src/serializer/struct_slots.dart
@@ -1,12 +1,6 @@
import 'package:meta/meta.dart';
import 'package:fory/src/serializer/serialization_field_info.dart';
-@internal
-final Object structWriteSlotsKey = Object();
-
-@internal
-final Object structReadSlotsKey = Object();
-
@internal
final class StructWriteSlots {
final List<SerializationFieldInfo> orderedFields;
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]