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 1cdcb42cf perf(csharp): add csharp benchmarks and optimize hot
serialization paths (#3396)
1cdcb42cf is described below
commit 1cdcb42cf37d1660bf1fab56ffd1b3f72406fc9e
Author: Shawn Yang <[email protected]>
AuthorDate: Tue Feb 24 12:08:02 2026 +0800
perf(csharp): add csharp benchmarks and optimize hot serialization paths
(#3396)
## Why?
- Add a first-class C# benchmark suite aligned with existing benchmark
datasets and serializers.
- Improve C# runtime hot paths while keeping benchmark comparisons fair.
- Fix enum behavior so undefined numeric enum values can round-trip
instead of failing.
## What does this PR do?
- Adds `benchmarks/csharp` benchmark harness (`Program.cs`, models,
serializer adapters, `run.sh`, and report generator) for `fory`,
`protobuf-net`, and `MessagePack`.
- Benchmarks cover `Struct`, `Sample`, `MediaContent` and list variants
for both serialize/deserialize.
- Uses directly serializable benchmark models for all three serializers
(removed protobuf conversion adapter overhead from timed path).
- Updates benchmark report generation to keep throughput and size
reporting separate, and adds a C++-style `Serialized Data Sizes (bytes)`
matrix.
- Optimizes C# runtime hot paths: reusable `ByteWriter`/`ByteReader`,
span-based string encode/decode path, and cached allowed wire-type sets.
- Improves generated/runtime schema handling: `CheckStructVersion` is
wired through contexts/generated serializers with cached no-trackRef
schema hash.
- Optimizes collection and enum hot paths: sealed collection element
types avoid redundant type-info writes; enum serializer uses cached maps
and preserves unknown numeric enum values on read.
- Simplifies config surface by removing skip-root/reflection-fallback
flags and setting default `MaxDepth` to `20`.
- Adds/updates tests for schema version behavior and undefined enum
round-trip.
## Related issues
#1017 #3387
#3397
## Does this PR introduce any user-facing change?
- [x] Does this PR introduce any public API change?
- [ ] Does this PR introduce any binary protocol compatibility change?
## Benchmark
- Command:
- `cd benchmarks/csharp && ./run.sh --duration 3 --warmup 1`
- Selected results (ops/sec):
| Datatype | Operation | fory | protobuf | msgpack |
| ---------------- | ----------- | ---------:| ---------:| ---------:|
| MediaContent | Serialize | 2,787,853 | 2,241,239 | 2,111,882 |
| MediaContent | Deserialize | 2,601,783 | 1,343,196 | 1,252,873 |
| MediaContentList | Serialize | 588,552 | 443,250 | 505,927 |
| MediaContentList | Deserialize | 570,945 | 289,274 | 279,718 |
- Aggregate throughput (mean ops/sec across all benchmark cases):
- `fory`: `5,278,631`
- `protobuf`: `2,020,540`
- `msgpack`: `1,982,782`
---
benchmarks/csharp/.gitignore | 4 +
benchmarks/csharp/BenchmarkModels.cs | 458 +++++++++++++++++++++++
benchmarks/csharp/BenchmarkSerializers.cs | 112 ++++++
benchmarks/csharp/Fory.CSharpBenchmark.csproj | 19 +
benchmarks/csharp/Program.cs | 377 +++++++++++++++++++
benchmarks/csharp/README.md | 80 ++++
benchmarks/csharp/benchmark_report.py | 189 ++++++++++
benchmarks/csharp/run.sh | 112 ++++++
csharp/src/Fory.Generator/ForyObjectGenerator.cs | 27 +-
csharp/src/Fory/ByteBuffer.cs | 173 +++++++--
csharp/src/Fory/CollectionSerializers.cs | 12 +-
csharp/src/Fory/Config.cs | 13 +-
csharp/src/Fory/Context.cs | 8 +
csharp/src/Fory/EnumSerializer.cs | 38 +-
csharp/src/Fory/Fory.cs | 46 ++-
csharp/src/Fory/StringSerializer.cs | 49 ++-
csharp/src/Fory/TypeResolver.cs | 18 +-
csharp/tests/Fory.Tests/ForyRuntimeTests.cs | 18 +-
18 files changed, 1653 insertions(+), 100 deletions(-)
diff --git a/benchmarks/csharp/.gitignore b/benchmarks/csharp/.gitignore
new file mode 100644
index 000000000..0e7fa6ce0
--- /dev/null
+++ b/benchmarks/csharp/.gitignore
@@ -0,0 +1,4 @@
+bin/
+obj/
+build/
+report/
diff --git a/benchmarks/csharp/BenchmarkModels.cs
b/benchmarks/csharp/BenchmarkModels.cs
new file mode 100644
index 000000000..8e4a8a5e2
--- /dev/null
+++ b/benchmarks/csharp/BenchmarkModels.cs
@@ -0,0 +1,458 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+using Apache.Fory;
+using MessagePack;
+using ProtoBuf;
+
+namespace Apache.Fory.Benchmarks.CSharp;
+
+[ForyObject]
+[MessagePackObject(keyAsPropertyName: true)]
+[ProtoContract]
+public sealed class NumericStruct
+{
+ [Field(Encoding = FieldEncoding.Fixed)]
+ [ProtoMember(1)]
+ public int F1 { get; set; }
+
+ [Field(Encoding = FieldEncoding.Fixed)]
+ [ProtoMember(2)]
+ public int F2 { get; set; }
+
+ [Field(Encoding = FieldEncoding.Fixed)]
+ [ProtoMember(3)]
+ public int F3 { get; set; }
+
+ [Field(Encoding = FieldEncoding.Fixed)]
+ [ProtoMember(4)]
+ public int F4 { get; set; }
+
+ [Field(Encoding = FieldEncoding.Fixed)]
+ [ProtoMember(5)]
+ public int F5 { get; set; }
+
+ [Field(Encoding = FieldEncoding.Fixed)]
+ [ProtoMember(6)]
+ public int F6 { get; set; }
+
+ [Field(Encoding = FieldEncoding.Fixed)]
+ [ProtoMember(7)]
+ public int F7 { get; set; }
+
+ [Field(Encoding = FieldEncoding.Fixed)]
+ [ProtoMember(8)]
+ public int F8 { get; set; }
+}
+
+[ForyObject]
+[MessagePackObject(keyAsPropertyName: true)]
+[ProtoContract]
+public sealed class StructList
+{
+ [ProtoMember(1)]
+ public List<NumericStruct> Values { get; set; } = [];
+}
+
+[ForyObject]
+[MessagePackObject(keyAsPropertyName: true)]
+[ProtoContract]
+public sealed class Sample
+{
+ [Field(Encoding = FieldEncoding.Fixed)]
+ [ProtoMember(1)]
+ public int IntValue { get; set; }
+
+ [Field(Encoding = FieldEncoding.Fixed)]
+ [ProtoMember(2)]
+ public long LongValue { get; set; }
+
+ [ProtoMember(3)]
+ public float FloatValue { get; set; }
+
+ [ProtoMember(4)]
+ public double DoubleValue { get; set; }
+
+ [Field(Encoding = FieldEncoding.Fixed)]
+ [ProtoMember(5)]
+ public int ShortValue { get; set; }
+
+ [Field(Encoding = FieldEncoding.Fixed)]
+ [ProtoMember(6)]
+ public int CharValue { get; set; }
+
+ [ProtoMember(7)]
+ public bool BooleanValue { get; set; }
+
+ [Field(Encoding = FieldEncoding.Fixed)]
+ [ProtoMember(8)]
+ public int IntValueBoxed { get; set; }
+
+ [Field(Encoding = FieldEncoding.Fixed)]
+ [ProtoMember(9)]
+ public long LongValueBoxed { get; set; }
+
+ [ProtoMember(10)]
+ public float FloatValueBoxed { get; set; }
+
+ [ProtoMember(11)]
+ public double DoubleValueBoxed { get; set; }
+
+ [Field(Encoding = FieldEncoding.Fixed)]
+ [ProtoMember(12)]
+ public int ShortValueBoxed { get; set; }
+
+ [Field(Encoding = FieldEncoding.Fixed)]
+ [ProtoMember(13)]
+ public int CharValueBoxed { get; set; }
+
+ [ProtoMember(14)]
+ public bool BooleanValueBoxed { get; set; }
+
+ [ProtoMember(15)]
+ public int[] IntArray { get; set; } = [];
+
+ [ProtoMember(16)]
+ public long[] LongArray { get; set; } = [];
+
+ [ProtoMember(17)]
+ public float[] FloatArray { get; set; } = [];
+
+ [ProtoMember(18)]
+ public double[] DoubleArray { get; set; } = [];
+
+ [ProtoMember(19)]
+ public int[] ShortArray { get; set; } = [];
+
+ [ProtoMember(20)]
+ public int[] CharArray { get; set; } = [];
+
+ [ProtoMember(21)]
+ public bool[] BooleanArray { get; set; } = [];
+
+ [ProtoMember(22)]
+ public string String { get; set; } = string.Empty;
+}
+
+[ForyObject]
+[MessagePackObject(keyAsPropertyName: true)]
+[ProtoContract]
+public sealed class SampleList
+{
+ [ProtoMember(1)]
+ public List<Sample> Values { get; set; } = [];
+}
+
+[ForyObject]
+[ProtoContract]
+public enum Player
+{
+ [ProtoEnum]
+ Java,
+ [ProtoEnum]
+ Flash,
+}
+
+[ForyObject]
+[ProtoContract]
+public enum MediaSize
+{
+ [ProtoEnum]
+ Small,
+ [ProtoEnum]
+ Large,
+}
+
+[ForyObject]
+[MessagePackObject(keyAsPropertyName: true)]
+[ProtoContract]
+public sealed class Media
+{
+ [ProtoMember(1)]
+ public string Uri { get; set; } = string.Empty;
+
+ [ProtoMember(2)]
+ public string Title { get; set; } = string.Empty;
+
+ [Field(Encoding = FieldEncoding.Fixed)]
+ [ProtoMember(3)]
+ public int Width { get; set; }
+
+ [Field(Encoding = FieldEncoding.Fixed)]
+ [ProtoMember(4)]
+ public int Height { get; set; }
+
+ [ProtoMember(5)]
+ public string Format { get; set; } = string.Empty;
+
+ [Field(Encoding = FieldEncoding.Fixed)]
+ [ProtoMember(6)]
+ public long Duration { get; set; }
+
+ [Field(Encoding = FieldEncoding.Fixed)]
+ [ProtoMember(7)]
+ public long Size { get; set; }
+
+ [Field(Encoding = FieldEncoding.Fixed)]
+ [ProtoMember(8)]
+ public int Bitrate { get; set; }
+
+ [ProtoMember(9)]
+ public bool HasBitrate { get; set; }
+
+ [ProtoMember(10)]
+ public List<string> Persons { get; set; } = [];
+
+ [ProtoMember(11)]
+ public Player Player { get; set; }
+
+ [ProtoMember(12)]
+ public string Copyright { get; set; } = string.Empty;
+}
+
+[ForyObject]
+[MessagePackObject(keyAsPropertyName: true)]
+[ProtoContract]
+public sealed class Image
+{
+ [ProtoMember(1)]
+ public string Uri { get; set; } = string.Empty;
+
+ [ProtoMember(2)]
+ public string Title { get; set; } = string.Empty;
+
+ [Field(Encoding = FieldEncoding.Fixed)]
+ [ProtoMember(3)]
+ public int Width { get; set; }
+
+ [Field(Encoding = FieldEncoding.Fixed)]
+ [ProtoMember(4)]
+ public int Height { get; set; }
+
+ [ProtoMember(5)]
+ public MediaSize Size { get; set; }
+}
+
+[ForyObject]
+[MessagePackObject(keyAsPropertyName: true)]
+[ProtoContract]
+public sealed class MediaContent
+{
+ [ProtoMember(1)]
+ public Media Media { get; set; } = new();
+
+ [ProtoMember(2)]
+ public List<Image> Images { get; set; } = [];
+}
+
+[ForyObject]
+[MessagePackObject(keyAsPropertyName: true)]
+[ProtoContract]
+public sealed class MediaContentList
+{
+ [ProtoMember(1)]
+ public List<MediaContent> Values { get; set; } = [];
+}
+
+public static class BenchmarkDataFactory
+{
+ private const int ListSize = 5;
+
+ public static NumericStruct CreateNumericStruct()
+ {
+ return new NumericStruct
+ {
+ F1 = -12345,
+ F2 = 987654321,
+ F3 = -31415,
+ F4 = 27182818,
+ F5 = -32000,
+ F6 = 1000000,
+ F7 = -999999999,
+ F8 = 42,
+ };
+ }
+
+ public static Sample CreateSample()
+ {
+ return new Sample
+ {
+ IntValue = 123,
+ LongValue = 1230000,
+ FloatValue = 12.345f,
+ DoubleValue = 1.234567,
+ ShortValue = 12345,
+ CharValue = '!',
+ BooleanValue = true,
+ IntValueBoxed = 321,
+ LongValueBoxed = 3210000,
+ FloatValueBoxed = 54.321f,
+ DoubleValueBoxed = 7.654321,
+ ShortValueBoxed = 32100,
+ CharValueBoxed = '$',
+ BooleanValueBoxed = false,
+ IntArray = [-1234, -123, -12, -1, 0, 1, 12, 123, 1234],
+ LongArray = [-123400, -12300, -1200, -100, 0, 100, 1200, 12300,
123400],
+ FloatArray = [-12.34f, -12.3f, -12.0f, -1.0f, 0.0f, 1.0f, 12.0f,
12.3f, 12.34f],
+ DoubleArray = [-1.234, -1.23, -12.0, -1.0, 0.0, 1.0, 12.0, 1.23,
1.234],
+ ShortArray = [-1234, -123, -12, -1, 0, 1, 12, 123, 1234],
+ CharArray = ['a', 's', 'd', 'f', 'A', 'S', 'D', 'F'],
+ BooleanArray = [true, false, false, true],
+ String = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
+ };
+ }
+
+ public static MediaContent CreateMediaContent()
+ {
+ return new MediaContent
+ {
+ Media = new Media
+ {
+ Uri = "https://apache.org/fory/video/123",
+ Title = "Apache Fory: Cross-language serialization benchmark",
+ Width = 1920,
+ Height = 1080,
+ Format = "video/mp4",
+ Duration = 145_000,
+ Size = 58_000_000,
+ Bitrate = 3_200,
+ HasBitrate = true,
+ Persons = ["alice", "bob", "charlie", "david"],
+ Player = Player.Java,
+ Copyright = "Apache Software Foundation",
+ },
+ Images =
+ [
+ new Image
+ {
+ Uri = "https://apache.org/fory/image/1",
+ Title = "cover",
+ Width = 1920,
+ Height = 1080,
+ Size = MediaSize.Large,
+ },
+ new Image
+ {
+ Uri = "https://apache.org/fory/image/2",
+ Title = "thumbnail",
+ Width = 320,
+ Height = 180,
+ Size = MediaSize.Small,
+ },
+ ],
+ };
+ }
+
+ public static StructList CreateStructList()
+ {
+ NumericStruct value = CreateNumericStruct();
+ StructList list = new();
+ for (int i = 0; i < ListSize; i++)
+ {
+ list.Values.Add(
+ new NumericStruct
+ {
+ F1 = value.F1 + i,
+ F2 = value.F2 - i,
+ F3 = value.F3 + i,
+ F4 = value.F4 - i,
+ F5 = value.F5 + i,
+ F6 = value.F6 - i,
+ F7 = value.F7 + i,
+ F8 = value.F8 + i,
+ });
+ }
+
+ return list;
+ }
+
+ public static SampleList CreateSampleList()
+ {
+ Sample sample = CreateSample();
+ SampleList list = new();
+ for (int i = 0; i < ListSize; i++)
+ {
+ list.Values.Add(new Sample
+ {
+ IntValue = sample.IntValue + i,
+ LongValue = sample.LongValue + i,
+ FloatValue = sample.FloatValue,
+ DoubleValue = sample.DoubleValue,
+ ShortValue = sample.ShortValue,
+ CharValue = sample.CharValue,
+ BooleanValue = sample.BooleanValue,
+ IntValueBoxed = sample.IntValueBoxed,
+ LongValueBoxed = sample.LongValueBoxed,
+ FloatValueBoxed = sample.FloatValueBoxed,
+ DoubleValueBoxed = sample.DoubleValueBoxed,
+ ShortValueBoxed = sample.ShortValueBoxed,
+ CharValueBoxed = sample.CharValueBoxed,
+ BooleanValueBoxed = sample.BooleanValueBoxed,
+ IntArray = sample.IntArray,
+ LongArray = sample.LongArray,
+ FloatArray = sample.FloatArray,
+ DoubleArray = sample.DoubleArray,
+ ShortArray = sample.ShortArray,
+ CharArray = sample.CharArray,
+ BooleanArray = sample.BooleanArray,
+ String = sample.String,
+ });
+ }
+
+ return list;
+ }
+
+ public static MediaContentList CreateMediaContentList()
+ {
+ MediaContent content = CreateMediaContent();
+ MediaContentList list = new();
+ for (int i = 0; i < ListSize; i++)
+ {
+ list.Values.Add(new MediaContent
+ {
+ Media = new Media
+ {
+ Uri = content.Media.Uri,
+ Title = content.Media.Title,
+ Width = content.Media.Width,
+ Height = content.Media.Height,
+ Format = content.Media.Format,
+ Duration = content.Media.Duration,
+ Size = content.Media.Size,
+ Bitrate = content.Media.Bitrate,
+ HasBitrate = content.Media.HasBitrate,
+ Persons = [.. content.Media.Persons],
+ Player = content.Media.Player,
+ Copyright = content.Media.Copyright,
+ },
+ Images =
+ [
+ .. content.Images.Select(image => new Image
+ {
+ Uri = image.Uri,
+ Title = image.Title,
+ Width = image.Width,
+ Height = image.Height,
+ Size = image.Size,
+ }),
+ ],
+ });
+ }
+
+ return list;
+ }
+}
diff --git a/benchmarks/csharp/BenchmarkSerializers.cs
b/benchmarks/csharp/BenchmarkSerializers.cs
new file mode 100644
index 000000000..6f6c70393
--- /dev/null
+++ b/benchmarks/csharp/BenchmarkSerializers.cs
@@ -0,0 +1,112 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+using Apache.Fory;
+using MessagePack;
+using MessagePack.Resolvers;
+using ProtoBuf;
+using ForyRuntime = Apache.Fory.Fory;
+using ProtobufNetSerializer = ProtoBuf.Serializer;
+
+namespace Apache.Fory.Benchmarks.CSharp;
+
+internal interface IBenchmarkSerializer<T>
+{
+ string Name { get; }
+
+ byte[] Serialize(T value);
+
+ T Deserialize(byte[] payload);
+}
+
+internal sealed class ForySerializer<T> : IBenchmarkSerializer<T>
+{
+ private readonly ForyRuntime _fory = ForyRuntime.Builder().Build();
+
+ public ForySerializer()
+ {
+ BenchmarkTypeRegistry.RegisterAll(_fory);
+ }
+
+ public string Name => "fory";
+
+ public byte[] Serialize(T value)
+ {
+ return _fory.Serialize(value);
+ }
+
+ public T Deserialize(byte[] payload)
+ {
+ return _fory.Deserialize<T>(payload);
+ }
+}
+
+internal static class BenchmarkTypeRegistry
+{
+ public static void RegisterAll(ForyRuntime fory)
+ {
+ fory.Register<NumericStruct>(1000);
+ fory.Register<StructList>(1001);
+ fory.Register<Sample>(1002);
+ fory.Register<SampleList>(1003);
+ fory.Register<Media>(1004);
+ fory.Register<Image>(1005);
+ fory.Register<MediaContent>(1006);
+ fory.Register<MediaContentList>(1007);
+ fory.Register<Player>(1008);
+ fory.Register<MediaSize>(1009);
+ }
+}
+
+internal sealed class ProtobufSerializer<T> : IBenchmarkSerializer<T>
+{
+ private readonly MemoryStream _writeStream = new(256);
+
+ public string Name => "protobuf";
+
+ public byte[] Serialize(T value)
+ {
+ _writeStream.Position = 0;
+ _writeStream.SetLength(0);
+ ProtobufNetSerializer.Serialize(_writeStream, value);
+ return _writeStream.ToArray();
+ }
+
+ public T Deserialize(byte[] payload)
+ {
+ using MemoryStream stream = new(payload, writable: false);
+ return ProtobufNetSerializer.Deserialize<T>(stream);
+ }
+}
+
+internal sealed class MessagePackRuntimeSerializer<T> : IBenchmarkSerializer<T>
+{
+ private readonly MessagePackSerializerOptions _options =
TypelessContractlessStandardResolver.Options;
+
+ public string Name => "msgpack";
+
+ public byte[] Serialize(T value)
+ {
+ return MessagePackSerializer.Typeless.Serialize(value, _options);
+ }
+
+ public T Deserialize(byte[] payload)
+ {
+ object? value = MessagePackSerializer.Typeless.Deserialize(payload,
_options);
+ return (T)value!;
+ }
+}
diff --git a/benchmarks/csharp/Fory.CSharpBenchmark.csproj
b/benchmarks/csharp/Fory.CSharpBenchmark.csproj
new file mode 100644
index 000000000..7eebcba18
--- /dev/null
+++ b/benchmarks/csharp/Fory.CSharpBenchmark.csproj
@@ -0,0 +1,19 @@
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <OutputType>Exe</OutputType>
+ <TargetFramework>net8.0</TargetFramework>
+ <LangVersion>12.0</LangVersion>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>enable</Nullable>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="MessagePack" Version="2.5.172" />
+ <PackageReference Include="protobuf-net" Version="3.2.56" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="../../csharp/src/Fory/Fory.csproj" />
+ <ProjectReference
Include="../../csharp/src/Fory.Generator/Fory.Generator.csproj"
OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
+ </ItemGroup>
+</Project>
diff --git a/benchmarks/csharp/Program.cs b/benchmarks/csharp/Program.cs
new file mode 100644
index 000000000..3a3162f50
--- /dev/null
+++ b/benchmarks/csharp/Program.cs
@@ -0,0 +1,377 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+using System.Diagnostics;
+using System.Globalization;
+using System.Runtime.InteropServices;
+using System.Text.Json;
+
+namespace Apache.Fory.Benchmarks.CSharp;
+
+internal sealed record BenchmarkCase(
+ string Serializer,
+ string DataType,
+ string Operation,
+ int SerializedSize,
+ Action Action);
+
+internal sealed record BenchmarkResult(
+ string Serializer,
+ string DataType,
+ string Operation,
+ int SerializedSize,
+ double OperationsPerSecond,
+ double AverageNanoseconds,
+ long Iterations,
+ double ElapsedSeconds);
+
+internal sealed record BenchmarkOutput(
+ string GeneratedAtUtc,
+ string RuntimeVersion,
+ string OsDescription,
+ string OsArchitecture,
+ string ProcessArchitecture,
+ int ProcessorCount,
+ double WarmupSeconds,
+ double DurationSeconds,
+ List<BenchmarkResult> Results);
+
+internal sealed class BenchmarkOptions
+{
+ public HashSet<string> DataFilter { get; init; } = [];
+
+ public HashSet<string> SerializerFilter { get; init; } = [];
+
+ public double WarmupSeconds { get; init; } = 1.0;
+
+ public double DurationSeconds { get; init; } = 3.0;
+
+ public string OutputPath { get; init; } = "benchmark_results.json";
+
+ public bool ShowHelp { get; init; }
+
+ public static BenchmarkOptions Parse(string[] args)
+ {
+ HashSet<string> dataFilter = new(StringComparer.OrdinalIgnoreCase);
+ HashSet<string> serializerFilter =
new(StringComparer.OrdinalIgnoreCase);
+ double warmupSeconds = 1.0;
+ double durationSeconds = 3.0;
+ string outputPath = "benchmark_results.json";
+ bool showHelp = false;
+
+ for (int i = 0; i < args.Length; i++)
+ {
+ switch (args[i])
+ {
+ case "--help":
+ case "-h":
+ showHelp = true;
+ break;
+ case "--data":
+ RequireValue(args, i);
+ dataFilter.Add(args[++i]);
+ break;
+ case "--serializer":
+ RequireValue(args, i);
+ serializerFilter.Add(args[++i]);
+ break;
+ case "--warmup":
+ RequireValue(args, i);
+ warmupSeconds = ParsePositiveDouble(args[++i], "warmup");
+ break;
+ case "--duration":
+ RequireValue(args, i);
+ durationSeconds = ParsePositiveDouble(args[++i],
"duration");
+ break;
+ case "--output":
+ RequireValue(args, i);
+ outputPath = args[++i];
+ break;
+ default:
+ throw new ArgumentException($"unknown option: {args[i]}");
+ }
+ }
+
+ return new BenchmarkOptions
+ {
+ DataFilter = dataFilter,
+ SerializerFilter = serializerFilter,
+ WarmupSeconds = warmupSeconds,
+ DurationSeconds = durationSeconds,
+ OutputPath = outputPath,
+ ShowHelp = showHelp,
+ };
+ }
+
+ public bool IsDataEnabled(string dataType)
+ {
+ return DataFilter.Count == 0 || DataFilter.Contains(dataType);
+ }
+
+ public bool IsSerializerEnabled(string serializer)
+ {
+ return SerializerFilter.Count == 0 ||
SerializerFilter.Contains(serializer);
+ }
+
+ private static void RequireValue(string[] args, int index)
+ {
+ if (index + 1 >= args.Length)
+ {
+ throw new ArgumentException($"missing value for option
{args[index]}");
+ }
+ }
+
+ private static double ParsePositiveDouble(string text, string name)
+ {
+ if (!double.TryParse(text, NumberStyles.Float,
CultureInfo.InvariantCulture, out double value) || value <= 0)
+ {
+ throw new ArgumentException($"{name} must be a positive number,
got '{text}'");
+ }
+
+ return value;
+ }
+}
+
+internal static class Program
+{
+ private static object? _sink;
+
+ private static readonly JsonSerializerOptions JsonOptions = new()
+ {
+ WriteIndented = true,
+ };
+
+ private static int Main(string[] args)
+ {
+ BenchmarkOptions options;
+ try
+ {
+ options = BenchmarkOptions.Parse(args);
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"error: {ex.Message}");
+ PrintUsage();
+ return 1;
+ }
+
+ if (options.ShowHelp)
+ {
+ PrintUsage();
+ return 0;
+ }
+
+ List<BenchmarkCase> cases;
+ try
+ {
+ cases = BuildBenchmarkCases(options);
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"failed to build benchmarks: {ex}");
+ return 1;
+ }
+
+ if (cases.Count == 0)
+ {
+ Console.Error.WriteLine("no benchmark cases selected");
+ return 1;
+ }
+
+ Console.WriteLine("=== Fory C# Benchmark ===");
+ Console.WriteLine($"Cases: {cases.Count}");
+ Console.WriteLine($"Warmup: {options.WarmupSeconds.ToString("F2",
CultureInfo.InvariantCulture)}s");
+ Console.WriteLine($"Duration: {options.DurationSeconds.ToString("F2",
CultureInfo.InvariantCulture)}s");
+ Console.WriteLine();
+
+ List<BenchmarkResult> results = new(cases.Count);
+ foreach (BenchmarkCase benchmarkCase in cases)
+ {
+ Console.WriteLine($"Running
{benchmarkCase.Serializer}/{benchmarkCase.DataType}/{benchmarkCase.Operation}...");
+ BenchmarkResult result = RunBenchmarkCase(benchmarkCase,
options.WarmupSeconds, options.DurationSeconds);
+ results.Add(result);
+ }
+
+ BenchmarkOutput output = new(
+ GeneratedAtUtc: DateTime.UtcNow.ToString("O",
CultureInfo.InvariantCulture),
+ RuntimeVersion: Environment.Version.ToString(),
+ OsDescription: RuntimeInformation.OSDescription,
+ OsArchitecture: RuntimeInformation.OSArchitecture.ToString(),
+ ProcessArchitecture:
RuntimeInformation.ProcessArchitecture.ToString(),
+ ProcessorCount: Environment.ProcessorCount,
+ WarmupSeconds: options.WarmupSeconds,
+ DurationSeconds: options.DurationSeconds,
+ Results: results);
+
+ string outputPath = Path.GetFullPath(options.OutputPath);
+ string? parent = Path.GetDirectoryName(outputPath);
+ if (!string.IsNullOrEmpty(parent))
+ {
+ Directory.CreateDirectory(parent);
+ }
+
+ File.WriteAllText(outputPath, JsonSerializer.Serialize(output,
JsonOptions));
+
+ PrintSummary(results);
+ Console.WriteLine();
+ Console.WriteLine($"Results written to {outputPath}");
+ return 0;
+ }
+
+ private static void PrintUsage()
+ {
+ Console.WriteLine("Usage: dotnet run -c Release -- [OPTIONS]");
+ Console.WriteLine();
+ Console.WriteLine("Options:");
+ Console.WriteLine(" --data
<struct|sample|mediacontent|structlist|samplelist|mediacontentlist>");
+ Console.WriteLine(" --serializer <fory|protobuf|msgpack>");
+ Console.WriteLine(" --warmup <seconds>");
+ Console.WriteLine(" --duration <seconds>");
+ Console.WriteLine(" --output <path>");
+ Console.WriteLine(" --help");
+ }
+
+ private static BenchmarkResult RunBenchmarkCase(BenchmarkCase
benchmarkCase, double warmupSeconds, double durationSeconds)
+ {
+ RunForDuration(benchmarkCase.Action, warmupSeconds);
+
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ GC.Collect();
+
+ const int batchSize = 256;
+ long iterations = 0;
+ Stopwatch stopwatch = Stopwatch.StartNew();
+ while (stopwatch.Elapsed.TotalSeconds < durationSeconds)
+ {
+ for (int i = 0; i < batchSize; i++)
+ {
+ benchmarkCase.Action();
+ }
+
+ iterations += batchSize;
+ }
+
+ double elapsed = stopwatch.Elapsed.TotalSeconds;
+ double throughput = iterations / elapsed;
+ double nsPerOp = (elapsed * 1_000_000_000.0) / iterations;
+
+ return new BenchmarkResult(
+ benchmarkCase.Serializer,
+ benchmarkCase.DataType,
+ benchmarkCase.Operation,
+ benchmarkCase.SerializedSize,
+ throughput,
+ nsPerOp,
+ iterations,
+ elapsed);
+ }
+
+ private static void RunForDuration(Action action, double durationSeconds)
+ {
+ Stopwatch stopwatch = Stopwatch.StartNew();
+ while (stopwatch.Elapsed.TotalSeconds < durationSeconds)
+ {
+ action();
+ }
+ }
+
+ private static List<BenchmarkCase> BuildBenchmarkCases(BenchmarkOptions
options)
+ {
+ List<BenchmarkCase> cases = [];
+
+ AddCases("struct", BenchmarkDataFactory.CreateNumericStruct(),
options, cases);
+ AddCases("sample", BenchmarkDataFactory.CreateSample(), options,
cases);
+ AddCases("mediacontent", BenchmarkDataFactory.CreateMediaContent(),
options, cases);
+ AddCases("structlist", BenchmarkDataFactory.CreateStructList(),
options, cases);
+ AddCases("samplelist", BenchmarkDataFactory.CreateSampleList(),
options, cases);
+ AddCases("mediacontentlist",
BenchmarkDataFactory.CreateMediaContentList(), options, cases);
+
+ return cases;
+ }
+
+ private static void AddCases<T>(string dataType, T value, BenchmarkOptions
options, List<BenchmarkCase> cases)
+ {
+ if (!options.IsDataEnabled(dataType))
+ {
+ return;
+ }
+
+ List<IBenchmarkSerializer<T>> serializers = [];
+ if (options.IsSerializerEnabled("fory"))
+ {
+ serializers.Add(new ForySerializer<T>());
+ }
+
+ if (options.IsSerializerEnabled("protobuf"))
+ {
+ serializers.Add(new ProtobufSerializer<T>());
+ }
+
+ if (options.IsSerializerEnabled("msgpack"))
+ {
+ serializers.Add(new MessagePackRuntimeSerializer<T>());
+ }
+
+ foreach (IBenchmarkSerializer<T> serializer in serializers)
+ {
+ byte[] payload = serializer.Serialize(value);
+ _sink = serializer.Deserialize(payload);
+
+ cases.Add(new BenchmarkCase(
+ serializer.Name,
+ dataType,
+ "serialize",
+ payload.Length,
+ () =>
+ {
+ _sink = serializer.Serialize(value);
+ }));
+
+ cases.Add(new BenchmarkCase(
+ serializer.Name,
+ dataType,
+ "deserialize",
+ payload.Length,
+ () =>
+ {
+ _sink = serializer.Deserialize(payload);
+ }));
+ }
+ }
+
+ private static void PrintSummary(List<BenchmarkResult> results)
+ {
+ Console.WriteLine();
+ Console.WriteLine("=== Summary (ops/s) ===");
+
+ IEnumerable<IGrouping<string, BenchmarkResult>> groups = results
+ .OrderBy(r => r.DataType, StringComparer.Ordinal)
+ .ThenBy(r => r.Operation, StringComparer.Ordinal)
+ .GroupBy(r => $"{r.DataType}/{r.Operation}");
+
+ foreach (IGrouping<string, BenchmarkResult> group in groups)
+ {
+ Console.WriteLine(group.Key);
+ foreach (BenchmarkResult result in group.OrderByDescending(r =>
r.OperationsPerSecond))
+ {
+ Console.WriteLine(
+ $" {result.Serializer,-8}
{result.OperationsPerSecond,14:N0} ops/s {result.AverageNanoseconds,10:N1}
ns/op size={result.SerializedSize}");
+ }
+ }
+ }
+}
diff --git a/benchmarks/csharp/README.md b/benchmarks/csharp/README.md
new file mode 100644
index 000000000..c57e8c35c
--- /dev/null
+++ b/benchmarks/csharp/README.md
@@ -0,0 +1,80 @@
+# Fory C# Benchmark
+
+This benchmark compares Apache Fory C#, protobuf-net, and MessagePack-CSharp.
+
+Serializer setup used in this benchmark:
+
+- `fory`: `Fory.Builder().Build()`
+- `protobuf`: `protobuf-net` runtime serializer
+- `msgpack`: `MessagePackSerializer.Typeless` with
`TypelessContractlessStandardResolver`
+
+## Prerequisites
+
+- .NET SDK 8.0+
+- Python 3.8+
+
+## Quick Start
+
+```bash
+cd benchmarks/csharp
+./run.sh
+```
+
+This runs all benchmark cases and generates:
+
+- `build/benchmark_results.json`
+- `report/REPORT.md`
+
+## Run Options
+
+```bash
+./run.sh --help
+
+Options:
+ --data <struct|sample|mediacontent|structlist|samplelist|mediacontentlist>
+ --serializer <fory|protobuf|msgpack>
+ --duration <seconds>
+ --warmup <seconds>
+```
+
+Examples:
+
+```bash
+# Run only struct benchmarks
+./run.sh --data struct
+
+# Run only Fory benchmarks
+./run.sh --serializer fory
+
+# Use longer runs for stable numbers
+./run.sh --duration 10 --warmup 2
+```
+
+## Benchmark Cases
+
+- `struct`: 8-field integer object
+- `sample`: mixed primitive fields and arrays
+- `mediacontent`: nested object with list fields
+- `structlist`: list of struct payloads
+- `samplelist`: list of sample payloads
+- `mediacontentlist`: list of media content payloads
+
+Each case benchmarks:
+
+- serialize throughput
+- deserialize throughput
+
+## Results
+
+Latest run (`Darwin arm64`, `.NET 8.0.24`, `--duration 2 --warmup 0.5`):
+
+| Serializer | Mean ops/sec across all cases |
+| ---------- | ----------------------------: |
+| fory | 2,032,057 |
+| protobuf | 1,940,328 |
+| msgpack | 1,901,489 |
+
+Per-case winners vary by payload and operation. The full breakdown is
generated at:
+
+- `benchmarks/csharp/build/benchmark_results.json`
+- `benchmarks/csharp/report/REPORT.md`
diff --git a/benchmarks/csharp/benchmark_report.py
b/benchmarks/csharp/benchmark_report.py
new file mode 100755
index 000000000..8a06c1656
--- /dev/null
+++ b/benchmarks/csharp/benchmark_report.py
@@ -0,0 +1,189 @@
+#!/usr/bin/env python3
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import argparse
+import json
+import os
+from collections import defaultdict
+
+
+def parse_args() -> argparse.Namespace:
+ parser = argparse.ArgumentParser(
+ description="Generate markdown report from C# benchmark JSON output"
+ )
+ parser.add_argument(
+ "--json-file",
+ default="benchmark_results.json",
+ help="Benchmark JSON output file",
+ )
+ parser.add_argument("--output-dir", default="report", help="Output
directory")
+ return parser.parse_args()
+
+
+def load_results(path: str) -> dict:
+ with open(path, "r", encoding="utf-8") as f:
+ return json.load(f)
+
+
+def format_ops(value: float) -> str:
+ return f"{value:,.0f}"
+
+
+def format_ns(value: float) -> str:
+ return f"{value:,.1f}"
+
+
+def format_size(value: float) -> str:
+ return str(int(round(value)))
+
+
+def format_datatype_name(value: str) -> str:
+ mapping = {
+ "struct": "Struct",
+ "sample": "Sample",
+ "mediacontent": "MediaContent",
+ "structlist": "StructList",
+ "samplelist": "SampleList",
+ "mediacontentlist": "MediaContentList",
+ }
+ return mapping.get(value, value)
+
+
+def build_report(data: dict) -> str:
+ lines: list[str] = []
+ lines.append("# Fory C# Benchmark Report")
+ lines.append("")
+ lines.append("## Environment")
+ lines.append("")
+ lines.append(f"- Generated at (UTC): `{data['GeneratedAtUtc']}`")
+ lines.append(f"- Runtime version: `{data['RuntimeVersion']}`")
+ lines.append(f"- OS: `{data['OsDescription']}`")
+ lines.append(f"- OS architecture: `{data['OsArchitecture']}`")
+ lines.append(f"- Process architecture: `{data['ProcessArchitecture']}`")
+ lines.append(f"- CPU logical cores: `{data['ProcessorCount']}`")
+ lines.append(f"- Warmup seconds: `{data['WarmupSeconds']}`")
+ lines.append(f"- Duration seconds: `{data['DurationSeconds']}`")
+ lines.append("")
+
+ grouped: dict[tuple[str, str], list[dict]] = defaultdict(list)
+ for row in data["Results"]:
+ grouped[(row["DataType"], row["Operation"])].append(row)
+
+ lines.append("## Throughput Results")
+ lines.append("")
+
+ for key in sorted(grouped.keys()):
+ data_type, operation = key
+ rows = sorted(
+ grouped[key], key=lambda item: item["OperationsPerSecond"],
reverse=True
+ )
+ best = rows[0]
+
+ lines.append(f"### `{data_type}` / `{operation}`")
+ lines.append("")
+ lines.append("| Serializer | Ops/sec | ns/op | Relative to best |")
+ lines.append("| ---------- | ------: | ----: | ---------------: |")
+
+ for row in rows:
+ relative = best["OperationsPerSecond"] / row["OperationsPerSecond"]
+ lines.append(
+ "| "
+ f"{row['Serializer']}"
+ " | "
+ f"{format_ops(row['OperationsPerSecond'])}"
+ " | "
+ f"{format_ns(row['AverageNanoseconds'])}"
+ " | "
+ f"{relative:.2f}x"
+ " |"
+ )
+
+ lines.append("")
+
+ size_totals: dict[tuple[str, str], list[int]] = defaultdict(list)
+ for row in data["Results"]:
+ size_totals[(row["DataType"],
row["Serializer"])].append(row["SerializedSize"])
+
+ lines.append("## Size Comparison")
+ lines.append("")
+ lines.append("### Serialized Data Sizes (bytes)")
+ lines.append("")
+ lines.append("| Datatype | fory | protobuf | msgpack |")
+ lines.append("| -------- | ---- | -------- | ------- |")
+
+ size_by_data_type: dict[str, dict[str, float]] = defaultdict(dict)
+ for (data_type, serializer), values in size_totals.items():
+ size_by_data_type[data_type][serializer] = sum(values) / len(values)
+
+ preferred_order = [
+ "struct",
+ "sample",
+ "mediacontent",
+ "structlist",
+ "samplelist",
+ "mediacontentlist",
+ ]
+ remaining = sorted(
+ key for key in size_by_data_type.keys() if key not in preferred_order
+ )
+ data_type_order = [
+ key for key in preferred_order if key in size_by_data_type
+ ] + remaining
+ serializers = ["fory", "protobuf", "msgpack"]
+ for data_type in data_type_order:
+ cells = [format_datatype_name(data_type)]
+ for serializer in serializers:
+ size = size_by_data_type[data_type].get(serializer)
+ cells.append("-" if size is None else format_size(size))
+ lines.append(f"| {cells[0]} | {cells[1]} | {cells[2]} | {cells[3]} |")
+
+ lines.append("")
+
+ serializer_totals: dict[str, list[float]] = defaultdict(list)
+ for row in data["Results"]:
+ serializer_totals[row["Serializer"]].append(row["OperationsPerSecond"])
+
+ lines.append("## Aggregate Throughput")
+ lines.append("")
+ lines.append("| Serializer | Mean ops/sec across all cases |")
+ lines.append("| ---------- | ----------------------------: |")
+ for serializer in sorted(serializer_totals.keys()):
+ values = serializer_totals[serializer]
+ mean_value = sum(values) / len(values)
+ lines.append(f"| {serializer} | {format_ops(mean_value)} |")
+
+ lines.append("")
+ return "\n".join(lines)
+
+
+def main() -> None:
+ args = parse_args()
+ result = load_results(args.json_file)
+
+ os.makedirs(args.output_dir, exist_ok=True)
+ report_path = os.path.join(args.output_dir, "REPORT.md")
+
+ report = build_report(result)
+ with open(report_path, "w", encoding="utf-8") as f:
+ f.write(report)
+
+ print(f"Report written to {report_path}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/benchmarks/csharp/run.sh b/benchmarks/csharp/run.sh
new file mode 100755
index 000000000..fd95d4fb6
--- /dev/null
+++ b/benchmarks/csharp/run.sh
@@ -0,0 +1,112 @@
+#!/bin/bash
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+set -euo pipefail
+export ENABLE_FORY_DEBUG_OUTPUT=0
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+cd "$SCRIPT_DIR"
+
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+NC='\033[0m'
+
+DATA=""
+SERIALIZER=""
+DURATION="3"
+WARMUP="1"
+
+usage() {
+ cat <<USAGE
+Usage: $0 [OPTIONS]
+
+Build and run C# benchmarks.
+
+Options:
+ --data <struct|sample|mediacontent|structlist|samplelist|mediacontentlist>
+ Filter benchmark by data type
+ --serializer <fory|protobuf|msgpack>
+ Filter benchmark by serializer
+ --duration <seconds> Measure duration per benchmark (default: 3)
+ --warmup <seconds> Warmup duration per benchmark (default: 1)
+ --help Show this help
+USAGE
+ exit 0
+}
+
+while [[ $# -gt 0 ]]; do
+ case $1 in
+ --data)
+ DATA="$2"
+ shift 2
+ ;;
+ --serializer)
+ SERIALIZER="$2"
+ shift 2
+ ;;
+ --duration)
+ DURATION="$2"
+ shift 2
+ ;;
+ --warmup)
+ WARMUP="$2"
+ shift 2
+ ;;
+ --help|-h)
+ usage
+ ;;
+ *)
+ echo -e "${RED}Unknown option: $1${NC}"
+ usage
+ ;;
+ esac
+done
+
+mkdir -p build report
+RESULT_JSON="build/benchmark_results.json"
+
+RUN_ARGS=(
+ --output "$RESULT_JSON"
+ --duration "$DURATION"
+ --warmup "$WARMUP"
+)
+
+if [[ -n "$DATA" ]]; then
+ RUN_ARGS+=(--data "$DATA")
+fi
+
+if [[ -n "$SERIALIZER" ]]; then
+ RUN_ARGS+=(--serializer "$SERIALIZER")
+fi
+
+echo -e "${GREEN}=== Fory C# Benchmark ===${NC}"
+echo ""
+
+echo -e "${YELLOW}[1/3] Restoring dependencies...${NC}"
+dotnet restore ./Fory.CSharpBenchmark.csproj >/dev/null
+
+echo -e "${YELLOW}[2/3] Running benchmark...${NC}"
+dotnet run -c Release --project ./Fory.CSharpBenchmark.csproj --
"${RUN_ARGS[@]}"
+
+echo -e "${YELLOW}[3/3] Generating report...${NC}"
+python3 benchmark_report.py --json-file "$RESULT_JSON" --output-dir report
+
+echo ""
+echo -e "${GREEN}=== All done! ===${NC}"
+echo "Report generated at: $SCRIPT_DIR/report/REPORT.md"
diff --git a/csharp/src/Fory.Generator/ForyObjectGenerator.cs
b/csharp/src/Fory.Generator/ForyObjectGenerator.cs
index 8c6ae679f..5d2cdb09a 100644
--- a/csharp/src/Fory.Generator/ForyObjectGenerator.cs
+++ b/csharp/src/Fory.Generator/ForyObjectGenerator.cs
@@ -286,11 +286,24 @@ public sealed class ForyObjectGenerator :
IIncrementalGenerator
sb.AppendLine(" return
context.TypeResolver.GetSerializer<T>().Read(context, refMode, readTypeInfo);");
sb.AppendLine(" }");
sb.AppendLine();
+ sb.AppendLine(" private static uint? __ForySchemaHashNoTrackRef;");
+ sb.AppendLine();
sb.AppendLine(" private static uint __ForySchemaHash(bool trackRef,
global::Apache.Fory.TypeResolver typeResolver)");
sb.AppendLine(" {");
- sb.Append(" return
global::Apache.Fory.SchemaHash.StructHash32(");
+ sb.AppendLine(" if (!trackRef &&
__ForySchemaHashNoTrackRef.HasValue)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" return __ForySchemaHashNoTrackRef.Value;");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ sb.Append(" uint value =
global::Apache.Fory.SchemaHash.StructHash32(");
sb.Append(BuildSchemaFingerprintExpression(model.Members));
sb.AppendLine(");");
+ sb.AppendLine(" if (!trackRef)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" __ForySchemaHashNoTrackRef = value;");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ sb.AppendLine(" return value;");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" public override global::Apache.Fory.TypeId
StaticTypeId => global::Apache.Fory.TypeId.Struct;");
@@ -350,7 +363,8 @@ public sealed class ForyObjectGenerator :
IIncrementalGenerator
sb.AppendLine(" }");
sb.AppendLine();
- sb.AppendLine("
context.Writer.WriteInt32(unchecked((int)__ForySchemaHash(context.TrackRef,
context.TypeResolver)));");
+ sb.AppendLine(" uint schemaHash =
__ForySchemaHash(context.TrackRef, context.TypeResolver);");
+ sb.AppendLine("
context.Writer.WriteInt32(unchecked((int)schemaHash));");
foreach (MemberModel member in model.SortedMembers)
{
EmitWriteMember(sb, member, false);
@@ -393,10 +407,13 @@ public sealed class ForyObjectGenerator :
IIncrementalGenerator
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" uint schemaHash =
unchecked((uint)context.Reader.ReadInt32());");
- sb.AppendLine(" uint expectedHash =
__ForySchemaHash(context.TrackRef, context.TypeResolver);");
- sb.AppendLine(" if (schemaHash != expectedHash)");
+ sb.AppendLine(" if (context.CheckStructVersion)");
sb.AppendLine(" {");
- sb.AppendLine(" throw new
global::Apache.Fory.InvalidDataException($\"class version hash mismatch:
expected {expectedHash}, got {schemaHash}\");");
+ sb.AppendLine(" uint expectedHash =
__ForySchemaHash(context.TrackRef, context.TypeResolver);");
+ sb.AppendLine(" if (schemaHash != expectedHash)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" throw new
global::Apache.Fory.InvalidDataException($\"class version hash mismatch:
expected {expectedHash}, got {schemaHash}\");");
+ sb.AppendLine(" }");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine($" {model.TypeName} valueSchema = new
{model.TypeName}();");
diff --git a/csharp/src/Fory/ByteBuffer.cs b/csharp/src/Fory/ByteBuffer.cs
index 4d7e83e14..34a28748d 100644
--- a/csharp/src/Fory/ByteBuffer.cs
+++ b/csharp/src/Fory/ByteBuffer.cs
@@ -21,37 +21,41 @@ namespace Apache.Fory;
public sealed class ByteWriter
{
- private readonly List<byte> _storage;
+ private byte[] _storage;
+ private int _count;
public ByteWriter(int capacity = 256)
{
- _storage = new List<byte>(capacity);
+ _storage = new byte[Math.Max(1, capacity)];
+ _count = 0;
}
- public int Count => _storage.Count;
+ public int Count => _count;
- public IReadOnlyList<byte> Storage => _storage;
+ public IReadOnlyList<byte> Storage => new ArraySegment<byte>(_storage, 0,
_count);
public void Reserve(int additional)
{
- _storage.Capacity = Math.Max(_storage.Capacity, _storage.Count +
additional);
+ EnsureCapacity(additional);
}
public void WriteUInt8(byte value)
{
- _storage.Add(value);
+ EnsureCapacity(1);
+ _storage[_count] = value;
+ _count += 1;
}
public void WriteInt8(sbyte value)
{
- _storage.Add(unchecked((byte)value));
+ WriteUInt8(unchecked((byte)value));
}
public void WriteUInt16(ushort value)
{
- Span<byte> tmp = stackalloc byte[2];
- BinaryPrimitives.WriteUInt16LittleEndian(tmp, value);
- WriteBytes(tmp);
+ EnsureCapacity(2);
+ BinaryPrimitives.WriteUInt16LittleEndian(_storage.AsSpan(_count, 2),
value);
+ _count += 2;
}
public void WriteInt16(short value)
@@ -61,9 +65,9 @@ public sealed class ByteWriter
public void WriteUInt32(uint value)
{
- Span<byte> tmp = stackalloc byte[4];
- BinaryPrimitives.WriteUInt32LittleEndian(tmp, value);
- WriteBytes(tmp);
+ EnsureCapacity(4);
+ BinaryPrimitives.WriteUInt32LittleEndian(_storage.AsSpan(_count, 4),
value);
+ _count += 4;
}
public void WriteInt32(int value)
@@ -73,9 +77,9 @@ public sealed class ByteWriter
public void WriteUInt64(ulong value)
{
- Span<byte> tmp = stackalloc byte[8];
- BinaryPrimitives.WriteUInt64LittleEndian(tmp, value);
- WriteBytes(tmp);
+ EnsureCapacity(8);
+ BinaryPrimitives.WriteUInt64LittleEndian(_storage.AsSpan(_count, 8),
value);
+ _count += 8;
}
public void WriteInt64(long value)
@@ -85,32 +89,39 @@ public sealed class ByteWriter
public void WriteVarUInt32(uint value)
{
+ EnsureCapacity(5);
uint remaining = value;
while (remaining >= 0x80)
{
- WriteUInt8((byte)((remaining & 0x7F) | 0x80));
+ _storage[_count] = (byte)((remaining & 0x7F) | 0x80);
+ _count += 1;
remaining >>= 7;
}
- WriteUInt8((byte)remaining);
+ _storage[_count] = (byte)remaining;
+ _count += 1;
}
public void WriteVarUInt64(ulong value)
{
+ EnsureCapacity(10);
ulong remaining = value;
for (var i = 0; i < 8; i++)
{
if (remaining < 0x80)
{
- WriteUInt8((byte)remaining);
+ _storage[_count] = (byte)remaining;
+ _count += 1;
return;
}
- WriteUInt8((byte)((remaining & 0x7F) | 0x80));
+ _storage[_count] = (byte)((remaining & 0x7F) | 0x80);
+ _count += 1;
remaining >>= 7;
}
- WriteUInt8((byte)(remaining & 0xFF));
+ _storage[_count] = (byte)(remaining & 0xFF);
+ _count += 1;
}
public void WriteVarUInt36Small(ulong value)
@@ -171,50 +182,99 @@ public sealed class ByteWriter
public void WriteBytes(ReadOnlySpan<byte> bytes)
{
- for (int i = 0; i < bytes.Length; i++)
+ EnsureCapacity(bytes.Length);
+ bytes.CopyTo(_storage.AsSpan(_count));
+ _count += bytes.Length;
+ }
+
+ public Span<byte> GetSpan(int size)
+ {
+ if (size < 0)
{
- _storage.Add(bytes[i]);
+ throw new ArgumentOutOfRangeException(nameof(size));
}
+
+ EnsureCapacity(size);
+ return _storage.AsSpan(_count, size);
+ }
+
+ public void Advance(int count)
+ {
+ if (count < 0 || _count + count > _storage.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(count));
+ }
+
+ _count += count;
}
public void SetByte(int index, byte value)
{
+ if ((uint)index >= (uint)_count)
+ {
+ throw new ArgumentOutOfRangeException(nameof(index));
+ }
+
_storage[index] = value;
}
public void SetBytes(int index, ReadOnlySpan<byte> bytes)
{
- for (var i = 0; i < bytes.Length; i++)
+ if (index < 0 || index + bytes.Length > _count)
{
- _storage[index + i] = bytes[i];
+ throw new ArgumentOutOfRangeException(nameof(index));
}
+
+ bytes.CopyTo(_storage.AsSpan(index));
}
public byte[] ToArray()
{
- return _storage.ToArray();
+ byte[] result = new byte[_count];
+ Array.Copy(_storage, 0, result, 0, _count);
+ return result;
}
public void Reset()
{
- _storage.Clear();
+ _count = 0;
+ }
+
+ private void EnsureCapacity(int additional)
+ {
+ int required = _count + additional;
+ if (required <= _storage.Length)
+ {
+ return;
+ }
+
+ int next = _storage.Length * 2;
+ if (next < required)
+ {
+ next = required;
+ }
+
+ Array.Resize(ref _storage, next);
}
}
public sealed class ByteReader
{
- private readonly byte[] _storage;
+ private byte[] _storage;
+ private int _length;
private int _cursor;
public ByteReader(ReadOnlySpan<byte> data)
{
_storage = data.ToArray();
+ _length = _storage.Length;
_cursor = 0;
}
public ByteReader(byte[] bytes)
{
_storage = bytes;
+ _length = bytes.Length;
_cursor = 0;
}
@@ -222,7 +282,21 @@ public sealed class ByteReader
public int Cursor => _cursor;
- public int Remaining => _storage.Length - _cursor;
+ public int Remaining => _length - _cursor;
+
+ public void Reset(ReadOnlySpan<byte> data)
+ {
+ _storage = data.ToArray();
+ _length = _storage.Length;
+ _cursor = 0;
+ }
+
+ public void Reset(byte[] bytes)
+ {
+ _storage = bytes;
+ _length = bytes.Length;
+ _cursor = 0;
+ }
public void SetCursor(int value)
{
@@ -236,9 +310,9 @@ public sealed class ByteReader
public void CheckBound(int need)
{
- if (_cursor + need > _storage.Length)
+ if (_cursor + need > _length)
{
- throw new OutOfBoundsException(_cursor, need, _storage.Length);
+ throw new OutOfBoundsException(_cursor, need, _length);
}
}
@@ -296,14 +370,24 @@ public sealed class ByteReader
public uint ReadVarUInt32()
{
+ byte[] storage = _storage;
+ int cursor = _cursor;
+ int length = _length;
uint result = 0;
- var shift = 0;
+ int shift = 0;
while (true)
{
- byte b = ReadUInt8();
+ if (cursor >= length)
+ {
+ throw new OutOfBoundsException(cursor, 1, length);
+ }
+
+ byte b = storage[cursor];
+ cursor += 1;
result |= (uint)(b & 0x7F) << shift;
if ((b & 0x80) == 0)
{
+ _cursor = cursor;
return result;
}
@@ -317,22 +401,39 @@ public sealed class ByteReader
public ulong ReadVarUInt64()
{
+ byte[] storage = _storage;
+ int cursor = _cursor;
+ int length = _length;
ulong result = 0;
- var shift = 0;
+ int shift = 0;
for (var i = 0; i < 8; i++)
{
- byte b = ReadUInt8();
+ if (cursor >= length)
+ {
+ throw new OutOfBoundsException(cursor, 1, length);
+ }
+
+ byte b = storage[cursor];
+ cursor += 1;
result |= (ulong)(b & 0x7F) << shift;
if ((b & 0x80) == 0)
{
+ _cursor = cursor;
return result;
}
shift += 7;
}
- byte last = ReadUInt8();
+ if (cursor >= length)
+ {
+ throw new OutOfBoundsException(cursor, 1, length);
+ }
+
+ byte last = storage[cursor];
+ cursor += 1;
result |= (ulong)last << 56;
+ _cursor = cursor;
return result;
}
diff --git a/csharp/src/Fory/CollectionSerializers.cs
b/csharp/src/Fory/CollectionSerializers.cs
index 09d869b11..e5130c02c 100644
--- a/csharp/src/Fory/CollectionSerializers.cs
+++ b/csharp/src/Fory/CollectionSerializers.cs
@@ -31,6 +31,16 @@ internal static class CollectionBits
internal static class CollectionCodec
{
+ private static bool CanDeclareElementType<T>(TypeId staticTypeId)
+ {
+ if (!staticTypeId.NeedsTypeInfoForField())
+ {
+ return true;
+ }
+
+ return typeof(T).IsSealed;
+ }
+
public static void WriteCollectionData<T>(
IEnumerable<T> values,
Serializer<T> elementSerializer,
@@ -60,7 +70,7 @@ internal static class CollectionCodec
}
bool trackRef = context.TrackRef &&
elementSerializer.IsReferenceTrackableType;
- bool declaredElementType = hasGenerics &&
!elementSerializer.StaticTypeId.NeedsTypeInfoForField();
+ bool declaredElementType = hasGenerics &&
CanDeclareElementType<T>(elementSerializer.StaticTypeId);
bool dynamicElementType = elementSerializer.StaticTypeId ==
TypeId.Unknown;
byte header = dynamicElementType ? (byte)0 : CollectionBits.SameType;
diff --git a/csharp/src/Fory/Config.cs b/csharp/src/Fory/Config.cs
index 8dc88be42..5cde5d848 100644
--- a/csharp/src/Fory/Config.cs
+++ b/csharp/src/Fory/Config.cs
@@ -22,8 +22,7 @@ public sealed record Config(
bool TrackRef = false,
bool Compatible = false,
bool CheckStructVersion = false,
- bool EnableReflectionFallback = false,
- int MaxDepth = 512);
+ int MaxDepth = 20);
public sealed class ForyBuilder
{
@@ -31,8 +30,7 @@ public sealed class ForyBuilder
private bool _trackRef;
private bool _compatible;
private bool _checkStructVersion;
- private bool _enableReflectionFallback;
- private int _maxDepth = 512;
+ private int _maxDepth = 20;
public ForyBuilder Xlang(bool enabled = true)
{
@@ -58,12 +56,6 @@ public sealed class ForyBuilder
return this;
}
- public ForyBuilder EnableReflectionFallback(bool enabled = false)
- {
- _enableReflectionFallback = enabled;
- return this;
- }
-
public ForyBuilder MaxDepth(int value)
{
if (value <= 0)
@@ -82,7 +74,6 @@ public sealed class ForyBuilder
TrackRef: _trackRef,
Compatible: _compatible,
CheckStructVersion: _checkStructVersion,
- EnableReflectionFallback: _enableReflectionFallback,
MaxDepth: _maxDepth);
}
diff --git a/csharp/src/Fory/Context.cs b/csharp/src/Fory/Context.cs
index 138745306..f07466dee 100644
--- a/csharp/src/Fory/Context.cs
+++ b/csharp/src/Fory/Context.cs
@@ -149,6 +149,7 @@ public sealed class WriteContext
TypeResolver typeResolver,
bool trackRef,
bool compatible = false,
+ bool checkStructVersion = false,
CompatibleTypeDefWriteState? compatibleTypeDefState = null,
MetaStringWriteState? metaStringWriteState = null)
{
@@ -156,6 +157,7 @@ public sealed class WriteContext
TypeResolver = typeResolver;
TrackRef = trackRef;
Compatible = compatible;
+ CheckStructVersion = checkStructVersion;
RefWriter = new RefWriter();
CompatibleTypeDefState = compatibleTypeDefState ?? new
CompatibleTypeDefWriteState();
MetaStringWriteState = metaStringWriteState ?? new
MetaStringWriteState();
@@ -169,6 +171,8 @@ public sealed class WriteContext
public bool Compatible { get; }
+ public bool CheckStructVersion { get; }
+
public RefWriter RefWriter { get; }
public CompatibleTypeDefWriteState CompatibleTypeDefState { get; }
@@ -231,6 +235,7 @@ public sealed class ReadContext
TypeResolver typeResolver,
bool trackRef,
bool compatible = false,
+ bool checkStructVersion = false,
CompatibleTypeDefReadState? compatibleTypeDefState = null,
MetaStringReadState? metaStringReadState = null)
{
@@ -238,6 +243,7 @@ public sealed class ReadContext
TypeResolver = typeResolver;
TrackRef = trackRef;
Compatible = compatible;
+ CheckStructVersion = checkStructVersion;
RefReader = new RefReader();
CompatibleTypeDefState = compatibleTypeDefState ?? new
CompatibleTypeDefReadState();
MetaStringReadState = metaStringReadState ?? new MetaStringReadState();
@@ -251,6 +257,8 @@ public sealed class ReadContext
public bool Compatible { get; }
+ public bool CheckStructVersion { get; }
+
public RefReader RefReader { get; }
public CompatibleTypeDefReadState CompatibleTypeDefState { get; }
diff --git a/csharp/src/Fory/EnumSerializer.cs
b/csharp/src/Fory/EnumSerializer.cs
index 315573264..e830f0756 100644
--- a/csharp/src/Fory/EnumSerializer.cs
+++ b/csharp/src/Fory/EnumSerializer.cs
@@ -19,25 +19,53 @@ namespace Apache.Fory;
public sealed class EnumSerializer<TEnum> : Serializer<TEnum> where TEnum :
struct, Enum
{
+ private static readonly Dictionary<TEnum, uint> DefinedValueToOrdinal =
BuildValueToOrdinalMap();
+ private static readonly Dictionary<uint, TEnum> DefinedOrdinalToValue =
BuildOrdinalToValueMap(DefinedValueToOrdinal);
+
public override TypeId StaticTypeId => TypeId.Enum;
public override TEnum DefaultValue => default;
public override void WriteData(WriteContext context, in TEnum value, bool
hasGenerics)
{
_ = hasGenerics;
- uint ordinal = Convert.ToUInt32(value);
+ if (!DefinedValueToOrdinal.TryGetValue(value, out uint ordinal))
+ {
+ ordinal = Convert.ToUInt32(value);
+ }
+
context.Writer.WriteVarUInt32(ordinal);
}
public override TEnum ReadData(ReadContext context)
{
uint ordinal = context.Reader.ReadVarUInt32();
- TEnum value = (TEnum)Enum.ToObject(typeof(TEnum), ordinal);
- if (!Enum.IsDefined(typeof(TEnum), value))
+ if (DefinedOrdinalToValue.TryGetValue(ordinal, out TEnum value))
+ {
+ return value;
+ }
+
+ return (TEnum)Enum.ToObject(typeof(TEnum), ordinal);
+ }
+
+ private static Dictionary<TEnum, uint> BuildValueToOrdinalMap()
+ {
+ Dictionary<TEnum, uint> values = [];
+ foreach (TEnum value in Enum.GetValues<TEnum>())
+ {
+ values[value] = Convert.ToUInt32(value);
+ }
+
+ return values;
+ }
+
+ private static Dictionary<uint, TEnum>
BuildOrdinalToValueMap(Dictionary<TEnum, uint> valueToOrdinal)
+ {
+ Dictionary<uint, TEnum> ordinalToValue = [];
+ foreach (KeyValuePair<TEnum, uint> pair in valueToOrdinal)
{
- throw new InvalidDataException($"unknown enum ordinal {ordinal}");
+ ordinalToValue[pair.Value] = pair.Key;
}
- return value;
+ return ordinalToValue;
}
}
diff --git a/csharp/src/Fory/Fory.cs b/csharp/src/Fory/Fory.cs
index 643da63e2..d70cef7dd 100644
--- a/csharp/src/Fory/Fory.cs
+++ b/csharp/src/Fory/Fory.cs
@@ -39,6 +39,7 @@ public sealed class Fory
_typeResolver,
Config.TrackRef,
Config.Compatible,
+ Config.CheckStructVersion,
new CompatibleTypeDefWriteState(),
new MetaStringWriteState());
_readContext = new ReadContext(
@@ -46,6 +47,7 @@ public sealed class Fory
_typeResolver,
Config.TrackRef,
Config.Compatible,
+ Config.CheckStructVersion,
new CompatibleTypeDefReadState(),
new MetaStringReadState());
}
@@ -93,7 +95,8 @@ public sealed class Fory
public byte[] Serialize<T>(in T value)
{
- ByteWriter writer = new();
+ ByteWriter writer = _writeContext.Writer;
+ writer.Reset();
Serializer<T> serializer = _typeResolver.GetSerializer<T>();
bool isNone = serializer.IsNone(value);
WriteHead(writer, isNone);
@@ -116,7 +119,21 @@ public sealed class Fory
public T Deserialize<T>(ReadOnlySpan<byte> payload)
{
- ByteReader reader = new(payload);
+ ByteReader reader = _readContext.Reader;
+ reader.Reset(payload);
+ T value = DeserializeFromReader<T>(reader);
+ if (reader.Remaining != 0)
+ {
+ throw new InvalidDataException($"unexpected trailing bytes after
deserializing {typeof(T)}");
+ }
+
+ return value;
+ }
+
+ public T Deserialize<T>(byte[] payload)
+ {
+ ByteReader reader = _readContext.Reader;
+ reader.Reset(payload);
T value = DeserializeFromReader<T>(reader);
if (reader.Remaining != 0)
{
@@ -129,7 +146,8 @@ public sealed class Fory
public T Deserialize<T>(ref ReadOnlySequence<byte> payload)
{
byte[] bytes = payload.ToArray();
- ByteReader reader = new(bytes);
+ ByteReader reader = _readContext.Reader;
+ reader.Reset(bytes);
T value = DeserializeFromReader<T>(reader);
payload = payload.Slice(reader.Cursor);
return value;
@@ -137,7 +155,8 @@ public sealed class Fory
public byte[] SerializeObject(object? value)
{
- ByteWriter writer = new();
+ ByteWriter writer = _writeContext.Writer;
+ writer.Reset();
bool isNone = value is null;
WriteHead(writer, isNone);
if (!isNone)
@@ -159,7 +178,21 @@ public sealed class Fory
public object? DeserializeObject(ReadOnlySpan<byte> payload)
{
- ByteReader reader = new(payload);
+ ByteReader reader = _readContext.Reader;
+ reader.Reset(payload);
+ object? value = DeserializeObjectFromReader(reader);
+ if (reader.Remaining != 0)
+ {
+ throw new InvalidDataException("unexpected trailing bytes after
deserializing dynamic object");
+ }
+
+ return value;
+ }
+
+ public object? DeserializeObject(byte[] payload)
+ {
+ ByteReader reader = _readContext.Reader;
+ reader.Reset(payload);
object? value = DeserializeObjectFromReader(reader);
if (reader.Remaining != 0)
{
@@ -172,7 +205,8 @@ public sealed class Fory
public object? DeserializeObject(ref ReadOnlySequence<byte> payload)
{
byte[] bytes = payload.ToArray();
- ByteReader reader = new(bytes);
+ ByteReader reader = _readContext.Reader;
+ reader.Reset(bytes);
object? value = DeserializeObjectFromReader(reader);
payload = payload.Slice(reader.Cursor);
return value;
diff --git a/csharp/src/Fory/StringSerializer.cs
b/csharp/src/Fory/StringSerializer.cs
index e6ba3e69f..9dcf4ff5a 100644
--- a/csharp/src/Fory/StringSerializer.cs
+++ b/csharp/src/Fory/StringSerializer.cs
@@ -65,28 +65,22 @@ public sealed class StringSerializer : Serializer<string>
ulong header = context.Reader.ReadVarUInt36Small();
ulong encoding = header & 0x03;
int byteLength = checked((int)(header >> 2));
- byte[] bytes = context.Reader.ReadBytes(byteLength);
+ ReadOnlySpan<byte> bytes = context.Reader.ReadSpan(byteLength);
return encoding switch
- {
- (ulong)ForyStringEncoding.Utf8 => Encoding.UTF8.GetString(bytes),
- (ulong)ForyStringEncoding.Latin1 => DecodeLatin1(bytes),
+ {
+ (ulong)ForyStringEncoding.Utf8 =>
Encoding.UTF8.GetString(bytes),
+ (ulong)ForyStringEncoding.Latin1 => DecodeLatin1(bytes),
(ulong)ForyStringEncoding.Utf16 => DecodeUtf16(bytes),
_ => throw new EncodingException($"unsupported string encoding
{encoding}"),
};
}
- private static string DecodeLatin1(byte[] bytes)
+ private static string DecodeLatin1(ReadOnlySpan<byte> bytes)
{
- return string.Create(bytes.Length, bytes, static (span, b) =>
- {
- for (int i = 0; i < b.Length; i++)
- {
- span[i] = (char)b[i];
- }
- });
+ return Encoding.Latin1.GetString(bytes);
}
- private static string DecodeUtf16(byte[] bytes)
+ private static string DecodeUtf16(ReadOnlySpan<byte> bytes)
{
if ((bytes.Length & 1) != 0)
{
@@ -139,31 +133,34 @@ public sealed class StringSerializer : Serializer<string>
private static void WriteLatin1(WriteContext context, string value)
{
- byte[] latin1 = new byte[value.Length];
+ int byteLength = value.Length;
+ ulong header = ((ulong)byteLength << 2) |
(ulong)ForyStringEncoding.Latin1;
+ context.Writer.WriteVarUInt36Small(header);
+ Span<byte> latin1 = context.Writer.GetSpan(byteLength);
for (int i = 0; i < value.Length; i++)
{
latin1[i] = unchecked((byte)value[i]);
}
-
- WriteEncodedBytes(context, latin1, ForyStringEncoding.Latin1);
+ context.Writer.Advance(byteLength);
}
private static void WriteUtf8(WriteContext context, string value)
{
- byte[] utf8 = Encoding.UTF8.GetBytes(value);
- WriteEncodedBytes(context, utf8, ForyStringEncoding.Utf8);
+ int byteLength = Encoding.UTF8.GetByteCount(value);
+ ulong header = ((ulong)byteLength << 2) |
(ulong)ForyStringEncoding.Utf8;
+ context.Writer.WriteVarUInt36Small(header);
+ Span<byte> utf8 = context.Writer.GetSpan(byteLength);
+ int written = Encoding.UTF8.GetBytes(value, utf8);
+ context.Writer.Advance(written);
}
private static void WriteUtf16(WriteContext context, string value)
{
- byte[] utf16 = Encoding.Unicode.GetBytes(value);
- WriteEncodedBytes(context, utf16, ForyStringEncoding.Utf16);
- }
-
- private static void WriteEncodedBytes(WriteContext context, byte[] bytes,
ForyStringEncoding encoding)
- {
- ulong header = ((ulong)bytes.Length << 2) | (ulong)encoding;
+ int byteLength = Encoding.Unicode.GetByteCount(value);
+ ulong header = ((ulong)byteLength << 2) |
(ulong)ForyStringEncoding.Utf16;
context.Writer.WriteVarUInt36Small(header);
- context.Writer.WriteBytes(bytes);
+ Span<byte> utf16 = context.Writer.GetSpan(byteLength);
+ int written = Encoding.Unicode.GetBytes(value, utf16);
+ context.Writer.Advance(written);
}
}
diff --git a/csharp/src/Fory/TypeResolver.cs b/csharp/src/Fory/TypeResolver.cs
index c1bf9a731..62fd83c6c 100644
--- a/csharp/src/Fory/TypeResolver.cs
+++ b/csharp/src/Fory/TypeResolver.cs
@@ -23,6 +23,14 @@ namespace Apache.Fory;
public sealed class TypeResolver
{
private static readonly ConcurrentDictionary<Type, Func<Serializer>>
GeneratedFactories = new();
+ private static readonly ConcurrentDictionary<TypeId, HashSet<TypeId>>
SingleAllowedWireTypes = new();
+ private static readonly HashSet<TypeId> CompatibleStructAllowedWireTypes =
+ [
+ TypeId.Struct,
+ TypeId.NamedStruct,
+ TypeId.CompatibleStruct,
+ TypeId.NamedCompatibleStruct,
+ ];
private static readonly Dictionary<Type, Type>
PrimitiveStringKeyDictionaryCodecs = new()
{
[typeof(string)] = typeof(StringPrimitiveDictionaryCodec),
@@ -363,17 +371,13 @@ public sealed class TypeResolver
internal static HashSet<TypeId> AllowedWireTypeIds(TypeId declaredKind,
bool registerByName, bool compatible)
{
TypeId baseKind = NormalizeBaseKind(declaredKind);
- TypeId expected = ResolveWireTypeId(declaredKind, registerByName,
compatible);
- HashSet<TypeId> allowed = [expected];
if (baseKind == TypeId.Struct && compatible)
{
- allowed.Add(TypeId.CompatibleStruct);
- allowed.Add(TypeId.NamedCompatibleStruct);
- allowed.Add(TypeId.Struct);
- allowed.Add(TypeId.NamedStruct);
+ return CompatibleStructAllowedWireTypes;
}
- return allowed;
+ TypeId expected = ResolveWireTypeId(declaredKind, registerByName,
compatible);
+ return SingleAllowedWireTypes.GetOrAdd(expected, static typeId =>
[typeId]);
}
public object? ReadByUserTypeId(uint userTypeId, ReadContext context,
TypeMeta? compatibleTypeMeta = null)
diff --git a/csharp/tests/Fory.Tests/ForyRuntimeTests.cs
b/csharp/tests/Fory.Tests/ForyRuntimeTests.cs
index e4ea145e2..5a9e4e6c2 100644
--- a/csharp/tests/Fory.Tests/ForyRuntimeTests.cs
+++ b/csharp/tests/Fory.Tests/ForyRuntimeTests.cs
@@ -662,7 +662,7 @@ public sealed class ForyRuntimeTests
[Fact]
public void MacroFieldOrderFollowsForyRules()
{
- ForyRuntime fory = ForyRuntime.Builder().Build();
+ ForyRuntime fory =
ForyRuntime.Builder().CheckStructVersion(true).Build();
fory.Register<FieldOrder>(300);
FieldOrder value = new() { Z = "tail", A = 123_456_789, B = 17, C = 99
};
@@ -724,10 +724,10 @@ public sealed class ForyRuntimeTests
[Fact]
public void SchemaVersionMismatchThrows()
{
- ForyRuntime writer = ForyRuntime.Builder().Compatible(false).Build();
+ ForyRuntime writer =
ForyRuntime.Builder().Compatible(false).CheckStructVersion(true).Build();
writer.Register<OneStringField>(200);
- ForyRuntime reader = ForyRuntime.Builder().Compatible(false).Build();
+ ForyRuntime reader =
ForyRuntime.Builder().Compatible(false).CheckStructVersion(true).Build();
reader.Register<TwoStringField>(200);
byte[] payload = writer.Serialize(new OneStringField { F1 = "hello" });
@@ -766,6 +766,18 @@ public sealed class ForyRuntimeTests
Assert.Equal(value.Value, decoded.Value);
}
+ [Fact]
+ public void EnumRoundTripPreservesUndefinedValue()
+ {
+ ForyRuntime fory = ForyRuntime.Builder().Build();
+ fory.Register<TestColor>(100);
+
+ TestColor value = (TestColor)12345;
+ TestColor decoded = fory.Deserialize<TestColor>(fory.Serialize(value));
+ Assert.Equal(value, decoded);
+ Assert.Equal(12345u, Convert.ToUInt32(decoded));
+ }
+
[Fact]
public void DynamicObjectSupportsObjectKeyMapAndSet()
{
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]