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 b71db8a18 perf(dart): typed-container write fast path with scan
elimination (#3609)
b71db8a18 is described below
commit b71db8a187e2b56c82e572daea72120c0b359b68
Author: Yash Agarwal <[email protected]>
AuthorDate: Thu Apr 23 18:04:08 2026 +0530
perf(dart): typed-container write fast path with scan elimination (#3609)
## Why?
Writing `List<T>` / `Set<T>` struct fields paid an O(n) scan to detect
heterogeneous types and nulls. Codegen already knows the element type is
concrete and non-nullable — the scan is pure overhead.
## What does this PR do?
- Adds `writeTypedListPayload<T>` / `writeTypedSetPayload<T>` that
resolve `TypeInfo` once and write the header/type-meta without scanning.
- Adds `writeGeneratedDirectListValue<T>` /
`writeGeneratedDirectSetValue<T>` generated-code entry points.
- Adds a codegen predicate (gated on real Dart `DartType` nullability)
that emits the fast path for non-nullable, non-ref, non-dynamic list/set
fields; everything else keeps the existing slow path.
## 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?
No user-facing or wire-format change. Serialized bytes are
byte-identical to main.
## Benchmark
| Datatype | Operation | Fory TPS | Protobuf TPS | Fastest |
| ---------------- | ----------- | --------: | -----------: |
------------ |
| Struct | Serialize | 4,696,615 | 1,998,625 | fory (2.35x) |
| Struct | Deserialize | 5,815,245 | 4,173,568 | fory (1.39x) |
| Sample | Serialize | 1,744,871 | 481,801 | fory (3.62x) |
| Sample | Deserialize | 2,007,877 | 780,317 | fory (2.57x) |
| MediaContent | Serialize | 944,111 | 398,324 | fory (2.37x) |
| MediaContent | Deserialize | 1,457,065 | 675,724 | fory (2.16x) |
| StructList | Serialize | 1,981,716 | 351,853 | fory (5.63x) |
| StructList | Deserialize | 2,261,436 | 596,027 | fory (3.79x) |
| SampleList | Serialize | 426,153 | 46,590 | fory (9.15x) |
| SampleList | Deserialize | 479,900 | 99,694 | fory (4.81x) |
| MediaContentList | Serialize | 220,342 | 76,330 | fory (2.89x) |
| MediaContentList | Deserialize | 341,839 | 131,730 | fory (2.60x) |
---
.../fory/lib/src/codegen/fory_generator.dart | 42 ++++++++++++++++++
.../fory/lib/src/codegen/generated_support.dart | 42 ++++++++++++++++++
.../lib/src/serializer/collection_serializers.dart | 48 +++++++++++++++++++++
docs/benchmarks/dart/README.md | 46 ++++++++++----------
docs/benchmarks/dart/mediacontent.png | Bin 42586 -> 44517 bytes
docs/benchmarks/dart/mediacontentlist.png | Bin 53715 -> 51468 bytes
docs/benchmarks/dart/sample.png | Bin 46415 -> 46918 bytes
docs/benchmarks/dart/samplelist.png | Bin 45351 -> 53733 bytes
docs/benchmarks/dart/struct.png | Bin 32985 -> 32100 bytes
docs/benchmarks/dart/structlist.png | Bin 48417 -> 42590 bytes
docs/benchmarks/dart/throughput.png | Bin 87138 -> 79300 bytes
11 files changed, 155 insertions(+), 23 deletions(-)
diff --git a/dart/packages/fory/lib/src/codegen/fory_generator.dart
b/dart/packages/fory/lib/src/codegen/fory_generator.dart
index c6e9ad581..ae20f05ab 100644
--- a/dart/packages/fory/lib/src/codegen/fory_generator.dart
+++ b/dart/packages/fory/lib/src/codegen/fory_generator.dart
@@ -542,6 +542,10 @@ final class ForyGenerator extends Generator {
output.writeln(
' ${_directGeneratedWriteStatement(field,
'value.${field.name}')};',
);
+ } else if (_usesDirectGeneratedTypedContainerWriteFastPath(field)) {
+ output.writeln(
+ ' ${_directGeneratedTypedContainerWriteStatement(field, index,
'value.${field.name}')};',
+ );
} else {
final fieldValue = _generatedFieldInfoWriteValueExpression(
field,
@@ -1150,6 +1154,44 @@ GeneratedFieldType(
field.fieldType.typeId == TypeIds.map;
}
+ bool _usesDirectGeneratedTypedContainerWriteFastPath(
+ _GeneratedFieldSpec field,
+ ) {
+ if (field.fieldType.nullable ||
+ field.fieldType.ref ||
+ field.fieldType.dynamic == true) {
+ return false;
+ }
+ final typeId = field.fieldType.typeId;
+ if (typeId != TypeIds.list && typeId != TypeIds.set) {
+ return false;
+ }
+ final elementFieldType = field.fieldType.arguments.single;
+ if (elementFieldType.ref || elementFieldType.dynamic == true) {
+ return false;
+ }
+ final elementType = (field.type as InterfaceType).typeArguments.single;
+ return !_isNullable(elementType);
+ }
+
+ String _directGeneratedTypedContainerWriteStatement(
+ _GeneratedFieldSpec field,
+ int fieldIndex,
+ String valueExpression,
+ ) {
+ if (_isList(field.type)) {
+ final elementType = (field.type as InterfaceType).typeArguments.single;
+ return
'writeGeneratedDirectListValue<${_typeCodeString(elementType)}>(context,
fields[$fieldIndex], $valueExpression)';
+ }
+ if (_isSet(field.type)) {
+ final elementType = (field.type as InterfaceType).typeArguments.single;
+ return
'writeGeneratedDirectSetValue<${_typeCodeString(elementType)}>(context,
fields[$fieldIndex], $valueExpression)';
+ }
+ throw StateError(
+ 'Unsupported generated typed container write fast path for
${field.name}.',
+ );
+ }
+
List<_DirectGeneratedWriteReservationRun>
_directGeneratedWriteReservationRuns(List<_GeneratedFieldSpec> fields) {
final runs = <_DirectGeneratedWriteReservationRun>[];
diff --git a/dart/packages/fory/lib/src/codegen/generated_support.dart
b/dart/packages/fory/lib/src/codegen/generated_support.dart
index 645a0b86f..d3ff7290d 100644
--- a/dart/packages/fory/lib/src/codegen/generated_support.dart
+++ b/dart/packages/fory/lib/src/codegen/generated_support.dart
@@ -458,6 +458,48 @@ Object readGeneratedStructDirectValue(
return value;
}
+@internal
+void writeGeneratedDirectListValue<T>(
+ WriteContext context,
+ GeneratedStructFieldInfo field,
+ List<T> value,
+) {
+ final fieldType = field.fieldType;
+ if (fieldType.typeId != TypeIds.list ||
+ fieldType.nullable ||
+ fieldType.ref ||
+ fieldType.isDynamic) {
+ throw StateError('Field ${field.name} is not a direct list path.');
+ }
+ final elementFieldType = fieldType.arguments.single;
+ if (elementFieldType.ref || elementFieldType.isDynamic) {
+ throw StateError(
+ 'Field ${field.name} element type is not a direct list path.');
+ }
+ writeTypedListPayload<T>(context, value, elementFieldType);
+}
+
+@internal
+void writeGeneratedDirectSetValue<T>(
+ WriteContext context,
+ GeneratedStructFieldInfo field,
+ Set<T> value,
+) {
+ final fieldType = field.fieldType;
+ if (fieldType.typeId != TypeIds.set ||
+ fieldType.nullable ||
+ fieldType.ref ||
+ fieldType.isDynamic) {
+ throw StateError('Field ${field.name} is not a direct set path.');
+ }
+ final elementFieldType = fieldType.arguments.single;
+ if (elementFieldType.ref || elementFieldType.isDynamic) {
+ throw StateError(
+ 'Field ${field.name} element type is not a direct set path.');
+ }
+ writeTypedSetPayload<T>(context, value, elementFieldType);
+}
+
@internal
@pragma('vm:prefer-inline')
List<T> readGeneratedDirectListValue<T>(
diff --git a/dart/packages/fory/lib/src/serializer/collection_serializers.dart
b/dart/packages/fory/lib/src/serializer/collection_serializers.dart
index f65feb723..acec8fce4 100644
--- a/dart/packages/fory/lib/src/serializer/collection_serializers.dart
+++ b/dart/packages/fory/lib/src/serializer/collection_serializers.dart
@@ -475,6 +475,54 @@ Set<T> readTypedSetPayload<T>(
return Set<T>.of(readTypedListPayload(context, elementFieldType, convert));
}
+void writeTypedListPayload<T>(
+ WriteContext context,
+ List<T> values,
+ FieldType elementFieldType,
+) {
+ final size = values.length;
+ if (size > context.config.maxCollectionSize) {
+ throw StateError(
+ 'Collection size $size exceeds ${context.config.maxCollectionSize}.',
+ );
+ }
+ context.buffer.writeVarUint32(size);
+ if (size == 0) return;
+ final declaredTypeInfo =
context.typeResolver.resolveFieldType(elementFieldType);
+ final usesDeclaredType = usesDeclaredTypeInfo(
+ context.config.compatible,
+ elementFieldType,
+ declaredTypeInfo,
+ );
+ context.buffer.writeUint8(
+ _buildCollectionHeader(
+ trackRef: elementFieldType.ref,
+ hasNull: false,
+ usesDeclaredType: usesDeclaredType,
+ sameType: true,
+ ),
+ );
+ if (!usesDeclaredType) {
+ context.writeTypeMetaValue(declaredTypeInfo, values.first as Object);
+ }
+ _writeSameTypeElements(
+ context,
+ values,
+ declaredTypeInfo,
+ usesDeclaredType ? elementFieldType : null,
+ elementFieldType.ref,
+ false,
+ );
+}
+
+void writeTypedSetPayload<T>(
+ WriteContext context,
+ Set<T> values,
+ FieldType elementFieldType,
+) {
+ writeTypedListPayload<T>(context, values.toList(growable: false),
elementFieldType);
+}
+
int _buildCollectionHeader({
required bool trackRef,
required bool hasNull,
diff --git a/docs/benchmarks/dart/README.md b/docs/benchmarks/dart/README.md
index 2348854a2..cc8ae23ef 100644
--- a/docs/benchmarks/dart/README.md
+++ b/docs/benchmarks/dart/README.md
@@ -4,17 +4,17 @@ This benchmark compares serialization and deserialization
throughput for Apache
## Hardware and Runtime Info
-| Key | Value
|
-| --------------------- |
----------------------------------------------------------------- |
-| Timestamp | 2026-04-23T10:50:07.751368Z
|
-| OS | Version 15.7.2 (Build 24G325)
|
-| Host | MacBook-Pro.local
|
-| CPU Cores (Logical) | 12
|
-| Memory (GB) | 48.00
|
-| Dart | 3.10.7 (stable) (Tue Dec 23 00:01:57 2025 -0800) on
"macos_arm64" |
-| Samples per case | 5
|
-| Warmup per case (s) | 1.0
|
-| Duration per case (s) | 1.5
|
+| Key | Value
|
+| --------------------- |
---------------------------------------------------------------- |
+| Timestamp | 2026-04-23T12:21:28Z
|
+| OS | Version 26.2 (Build 25C56)
|
+| Host | Macbook-Air.local
|
+| CPU Cores (Logical) | 8
|
+| Memory (GB) | 8.00
|
+| Dart | 3.10.4 (stable) (Tue Dec 9 00:01:55 2025 -0800) on
"macos_arm64" |
+| Samples per case | 5
|
+| Warmup per case (s) | 1.0
|
+| Duration per case (s) | 1.5
|
## Throughput Results
@@ -22,18 +22,18 @@ This benchmark compares serialization and deserialization
throughput for Apache
| Datatype | Operation | Fory TPS | Protobuf TPS | Fastest |
| ---------------- | ----------- | --------: | -----------: | ------------ |
-| Struct | Serialize | 5,041,693 | 2,073,839 | fory (2.43x) |
-| Struct | Deserialize | 6,395,290 | 4,991,881 | fory (1.28x) |
-| Sample | Serialize | 1,783,688 | 552,140 | fory (3.23x) |
-| Sample | Deserialize | 2,124,197 | 934,794 | fory (2.27x) |
-| MediaContent | Serialize | 952,498 | 438,419 | fory (2.17x) |
-| MediaContent | Deserialize | 1,649,039 | 737,340 | fory (2.24x) |
-| StructList | Serialize | 1,945,119 | 399,007 | fory (4.87x) |
-| StructList | Deserialize | 2,119,403 | 764,832 | fory (2.77x) |
-| SampleList | Serialize | 475,413 | 52,512 | fory (9.05x) |
-| SampleList | Deserialize | 508,939 | 116,236 | fory (4.38x) |
-| MediaContentList | Serialize | 224,925 | 84,860 | fory (2.65x) |
-| MediaContentList | Deserialize | 387,070 | 154,392 | fory (2.51x) |
+| Struct | Serialize | 4,696,615 | 1,998,625 | fory (2.35x) |
+| Struct | Deserialize | 5,815,245 | 4,173,568 | fory (1.39x) |
+| Sample | Serialize | 1,744,871 | 481,801 | fory (3.62x) |
+| Sample | Deserialize | 2,007,877 | 780,317 | fory (2.57x) |
+| MediaContent | Serialize | 944,111 | 398,324 | fory (2.37x) |
+| MediaContent | Deserialize | 1,457,065 | 675,724 | fory (2.16x) |
+| StructList | Serialize | 1,981,716 | 351,853 | fory (5.63x) |
+| StructList | Deserialize | 2,261,436 | 596,027 | fory (3.79x) |
+| SampleList | Serialize | 426,153 | 46,590 | fory (9.15x) |
+| SampleList | Deserialize | 479,900 | 99,694 | fory (4.81x) |
+| MediaContentList | Serialize | 220,342 | 76,330 | fory (2.89x) |
+| MediaContentList | Deserialize | 341,839 | 131,730 | fory (2.60x) |
## Serialized Size (bytes)
diff --git a/docs/benchmarks/dart/mediacontent.png
b/docs/benchmarks/dart/mediacontent.png
index ba6e2d3ae..16f9b3a67 100644
Binary files a/docs/benchmarks/dart/mediacontent.png and
b/docs/benchmarks/dart/mediacontent.png differ
diff --git a/docs/benchmarks/dart/mediacontentlist.png
b/docs/benchmarks/dart/mediacontentlist.png
index 02f785e42..f88d84a9f 100644
Binary files a/docs/benchmarks/dart/mediacontentlist.png and
b/docs/benchmarks/dart/mediacontentlist.png differ
diff --git a/docs/benchmarks/dart/sample.png b/docs/benchmarks/dart/sample.png
index f274160b4..bd182feab 100644
Binary files a/docs/benchmarks/dart/sample.png and
b/docs/benchmarks/dart/sample.png differ
diff --git a/docs/benchmarks/dart/samplelist.png
b/docs/benchmarks/dart/samplelist.png
index 72e6cf7b0..a3b763501 100644
Binary files a/docs/benchmarks/dart/samplelist.png and
b/docs/benchmarks/dart/samplelist.png differ
diff --git a/docs/benchmarks/dart/struct.png b/docs/benchmarks/dart/struct.png
index f04b50825..84ac444fe 100644
Binary files a/docs/benchmarks/dart/struct.png and
b/docs/benchmarks/dart/struct.png differ
diff --git a/docs/benchmarks/dart/structlist.png
b/docs/benchmarks/dart/structlist.png
index 0bf1fd0df..1ed88798e 100644
Binary files a/docs/benchmarks/dart/structlist.png and
b/docs/benchmarks/dart/structlist.png differ
diff --git a/docs/benchmarks/dart/throughput.png
b/docs/benchmarks/dart/throughput.png
index d41f05638..f3e6ace25 100644
Binary files a/docs/benchmarks/dart/throughput.png and
b/docs/benchmarks/dart/throughput.png differ
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]