You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fory.apache.org by ch...@apache.org on 2026/03/04 07:56:07 UTC

(fory) branch main updated: perf(c#): add csharp benchmark results (#3451)

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 ee4dc7776 perf(c#): add csharp benchmark results (#3451)
ee4dc7776 is described below

commit ee4dc7776341af2a987ebfdf2c09d70f5a8e6484
Author: Shawn Yang <sh...@gmail.com>
AuthorDate: Wed Mar 4 15:56:01 2026 +0800

    perf(c#): add csharp benchmark results (#3451)
    
    ## Why?
    
    
    
    ## What does this PR do?
    - add csharp benchmark results
    - optimize c# performance
    
    
    ## Related issues
    
    #1017 #3387
    
    
    ## 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
---
 README.md                                        |  10 +
 benchmarks/csharp/BenchmarkModels.cs             | 193 +++----
 benchmarks/csharp/BenchmarkSerializers.cs        |  30 +-
 benchmarks/csharp/README.md                      |   8 +-
 benchmarks/csharp/benchmark_report.py            | 621 ++++++++++++++++++-----
 benchmarks/csharp/run.sh                         |  36 +-
 csharp/src/Fory.Generator/ForyObjectGenerator.cs | 509 ++++++++++++++++---
 csharp/src/Fory/Attributes.cs                    |   6 +
 csharp/src/Fory/ByteBuffer.cs                    | 262 ++++++++--
 csharp/src/Fory/CollectionSerializers.cs         |  97 +++-
 csharp/src/Fory/Context.cs                       | 244 ++++++++-
 csharp/src/Fory/DictionarySerializers.cs         |  12 +-
 csharp/src/Fory/Fory.cs                          |   3 +-
 csharp/src/Fory/NullableKeyDictionary.cs         |  12 +-
 csharp/src/Fory/Serializer.cs                    |   6 +
 csharp/src/Fory/StringSerializer.cs              |  90 ++--
 csharp/src/Fory/TypeInfo.cs                      | 140 ++++-
 csharp/src/Fory/TypeMeta.cs                      | 152 ++++++
 csharp/src/Fory/TypeResolver.cs                  | 469 +++++++++++++++--
 csharp/tests/Fory.Tests/ForyRuntimeTests.cs      | 312 ++++++++++++
 docs/benchmarks/csharp/README.md                 | 129 +++++
 docs/benchmarks/csharp/mediacontent.png          | Bin 0 -> 54916 bytes
 docs/benchmarks/csharp/mediacontentlist.png      | Bin 0 -> 51998 bytes
 docs/benchmarks/csharp/sample.png                | Bin 0 -> 50931 bytes
 docs/benchmarks/csharp/samplelist.png            | Bin 0 -> 55521 bytes
 docs/benchmarks/csharp/struct.png                | Bin 0 -> 58824 bytes
 docs/benchmarks/csharp/structlist.png            | Bin 0 -> 48137 bytes
 docs/benchmarks/csharp/throughput.png            | Bin 0 -> 83965 bytes
 28 files changed, 2816 insertions(+), 525 deletions(-)

diff --git a/README.md b/README.md
index 58c157f19..99a86c568 100644
--- a/README.md
+++ b/README.md
@@ -119,6 +119,16 @@ Fory Go demonstrates excellent performance compared to other go serialization fr
 
 For more detailed benchmarks and methodology, see [Go Benchmark](benchmarks/go).
 
+### C# Serialization Performance
+
+Fory C# demonstrates excellent performance compared to protobuf-net and MessagePack-CSharp:
+
+<p align="center">
+<img src="docs/benchmarks/csharp/throughput.png" width="95%">
+</p>
+
+For more detailed benchmarks and methodology, see [C# Benchmarks](docs/benchmarks/csharp).
+
 ## Installation
 
 **Java**:
diff --git a/benchmarks/csharp/BenchmarkModels.cs b/benchmarks/csharp/BenchmarkModels.cs
index 8e4a8a5e2..f3c2fc7c0 100644
--- a/benchmarks/csharp/BenchmarkModels.cs
+++ b/benchmarks/csharp/BenchmarkModels.cs
@@ -26,35 +26,35 @@ namespace Apache.Fory.Benchmarks.CSharp;
 [ProtoContract]
 public sealed class NumericStruct
 {
-    [Field(Encoding = FieldEncoding.Fixed)]
+    [Field(Id = 1)]
     [ProtoMember(1)]
     public int F1 { get; set; }
 
-    [Field(Encoding = FieldEncoding.Fixed)]
+    [Field(Id = 2)]
     [ProtoMember(2)]
     public int F2 { get; set; }
 
-    [Field(Encoding = FieldEncoding.Fixed)]
+    [Field(Id = 3)]
     [ProtoMember(3)]
     public int F3 { get; set; }
 
-    [Field(Encoding = FieldEncoding.Fixed)]
+    [Field(Id = 4)]
     [ProtoMember(4)]
     public int F4 { get; set; }
 
-    [Field(Encoding = FieldEncoding.Fixed)]
+    [Field(Id = 5)]
     [ProtoMember(5)]
     public int F5 { get; set; }
 
-    [Field(Encoding = FieldEncoding.Fixed)]
+    [Field(Id = 6)]
     [ProtoMember(6)]
     public int F6 { get; set; }
 
-    [Field(Encoding = FieldEncoding.Fixed)]
+    [Field(Id = 7)]
     [ProtoMember(7)]
     public int F7 { get; set; }
 
-    [Field(Encoding = FieldEncoding.Fixed)]
+    [Field(Id = 8)]
     [ProtoMember(8)]
     public int F8 { get; set; }
 }
@@ -64,6 +64,7 @@ public sealed class NumericStruct
 [ProtoContract]
 public sealed class StructList
 {
+    [Field(Id = 1)]
     [ProtoMember(1)]
     public List<NumericStruct> Values { get; set; } = [];
 }
@@ -73,77 +74,91 @@ public sealed class StructList
 [ProtoContract]
 public sealed class Sample
 {
-    [Field(Encoding = FieldEncoding.Fixed)]
+    [Field(Id = 1)]
     [ProtoMember(1)]
     public int IntValue { get; set; }
 
-    [Field(Encoding = FieldEncoding.Fixed)]
+    [Field(Id = 2)]
     [ProtoMember(2)]
     public long LongValue { get; set; }
 
+    [Field(Id = 3)]
     [ProtoMember(3)]
     public float FloatValue { get; set; }
 
+    [Field(Id = 4)]
     [ProtoMember(4)]
     public double DoubleValue { get; set; }
 
-    [Field(Encoding = FieldEncoding.Fixed)]
+    [Field(Id = 5)]
     [ProtoMember(5)]
     public int ShortValue { get; set; }
 
-    [Field(Encoding = FieldEncoding.Fixed)]
+    [Field(Id = 6)]
     [ProtoMember(6)]
     public int CharValue { get; set; }
 
+    [Field(Id = 7)]
     [ProtoMember(7)]
     public bool BooleanValue { get; set; }
 
-    [Field(Encoding = FieldEncoding.Fixed)]
+    [Field(Id = 8)]
     [ProtoMember(8)]
     public int IntValueBoxed { get; set; }
 
-    [Field(Encoding = FieldEncoding.Fixed)]
+    [Field(Id = 9)]
     [ProtoMember(9)]
     public long LongValueBoxed { get; set; }
 
+    [Field(Id = 10)]
     [ProtoMember(10)]
     public float FloatValueBoxed { get; set; }
 
+    [Field(Id = 11)]
     [ProtoMember(11)]
     public double DoubleValueBoxed { get; set; }
 
-    [Field(Encoding = FieldEncoding.Fixed)]
+    [Field(Id = 12)]
     [ProtoMember(12)]
     public int ShortValueBoxed { get; set; }
 
-    [Field(Encoding = FieldEncoding.Fixed)]
+    [Field(Id = 13)]
     [ProtoMember(13)]
     public int CharValueBoxed { get; set; }
 
+    [Field(Id = 14)]
     [ProtoMember(14)]
     public bool BooleanValueBoxed { get; set; }
 
+    [Field(Id = 15)]
     [ProtoMember(15)]
     public int[] IntArray { get; set; } = [];
 
+    [Field(Id = 16)]
     [ProtoMember(16)]
     public long[] LongArray { get; set; } = [];
 
+    [Field(Id = 17)]
     [ProtoMember(17)]
     public float[] FloatArray { get; set; } = [];
 
+    [Field(Id = 18)]
     [ProtoMember(18)]
     public double[] DoubleArray { get; set; } = [];
 
+    [Field(Id = 19)]
     [ProtoMember(19)]
     public int[] ShortArray { get; set; } = [];
 
+    [Field(Id = 20)]
     [ProtoMember(20)]
     public int[] CharArray { get; set; } = [];
 
+    [Field(Id = 21)]
     [ProtoMember(21)]
     public bool[] BooleanArray { get; set; } = [];
 
+    [Field(Id = 22)]
     [ProtoMember(22)]
     public string String { get; set; } = string.Empty;
 }
@@ -153,6 +168,7 @@ public sealed class Sample
 [ProtoContract]
 public sealed class SampleList
 {
+    [Field(Id = 1)]
     [ProtoMember(1)]
     public List<Sample> Values { get; set; } = [];
 }
@@ -182,44 +198,51 @@ public enum MediaSize
 [ProtoContract]
 public sealed class Media
 {
+    [Field(Id = 1)]
     [ProtoMember(1)]
     public string Uri { get; set; } = string.Empty;
 
+    [Field(Id = 2)]
     [ProtoMember(2)]
     public string Title { get; set; } = string.Empty;
 
-    [Field(Encoding = FieldEncoding.Fixed)]
+    [Field(Id = 3)]
     [ProtoMember(3)]
     public int Width { get; set; }
 
-    [Field(Encoding = FieldEncoding.Fixed)]
+    [Field(Id = 4)]
     [ProtoMember(4)]
     public int Height { get; set; }
 
+    [Field(Id = 5)]
     [ProtoMember(5)]
     public string Format { get; set; } = string.Empty;
 
-    [Field(Encoding = FieldEncoding.Fixed)]
+    [Field(Id = 6)]
     [ProtoMember(6)]
     public long Duration { get; set; }
 
-    [Field(Encoding = FieldEncoding.Fixed)]
+    [Field(Id = 7)]
     [ProtoMember(7)]
     public long Size { get; set; }
 
-    [Field(Encoding = FieldEncoding.Fixed)]
+    [Field(Id = 8)]
     [ProtoMember(8)]
     public int Bitrate { get; set; }
 
+    [Field(Id = 9)]
     [ProtoMember(9)]
     public bool HasBitrate { get; set; }
 
+    [Field(Id = 10)]
     [ProtoMember(10)]
     public List<string> Persons { get; set; } = [];
 
+    [Field(Id = 11)]
     [ProtoMember(11)]
     public Player Player { get; set; }
 
+    [Field(Id = 12)]
     [ProtoMember(12)]
     public string Copyright { get; set; } = string.Empty;
 }
@@ -229,20 +252,23 @@ public sealed class Media
 [ProtoContract]
 public sealed class Image
 {
+    [Field(Id = 1)]
     [ProtoMember(1)]
     public string Uri { get; set; } = string.Empty;
 
+    [Field(Id = 2)]
     [ProtoMember(2)]
     public string Title { get; set; } = string.Empty;
 
-    [Field(Encoding = FieldEncoding.Fixed)]
+    [Field(Id = 3)]
     [ProtoMember(3)]
     public int Width { get; set; }
 
-    [Field(Encoding = FieldEncoding.Fixed)]
+    [Field(Id = 4)]
     [ProtoMember(4)]
     public int Height { get; set; }
 
+    [Field(Id = 5)]
     [ProtoMember(5)]
     public MediaSize Size { get; set; }
 }
@@ -252,9 +278,11 @@ public sealed class Image
 [ProtoContract]
 public sealed class MediaContent
 {
+    [Field(Id = 1)]
     [ProtoMember(1)]
     public Media Media { get; set; } = new();
 
+    [Field(Id = 2)]
     [ProtoMember(2)]
     public List<Image> Images { get; set; } = [];
 }
@@ -264,6 +292,7 @@ public sealed class MediaContent
 [ProtoContract]
 public sealed class MediaContentList
 {
+    [Field(Id = 1)]
     [ProtoMember(1)]
     public List<MediaContent> Values { get; set; } = [];
 }
@@ -322,35 +351,43 @@ public static class BenchmarkDataFactory
         {
             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",
+                Uri = "http://javaone.com/keynote.ogg",
+                Title = string.Empty,
+                Width = 641,
+                Height = 481,
+                Format = "video/theora\u1234",
+                Duration = 18_000_001,
+                Size = 58_982_401,
+                Bitrate = 0,
+                HasBitrate = false,
+                Persons = ["Bill Gates, Jr.", "Steven Jobs"],
+                Player = Player.Flash,
+                Copyright = "Copyright (c) 2009, Scooby Dooby Doo",
             },
             Images =
             [
                 new Image
                 {
-                    Uri = "https://apache.org/fory/image/1",
-                    Title = "cover",
-                    Width = 1920,
-                    Height = 1080,
+                    Uri = "http://javaone.com/keynote_huge.jpg",
+                    Title = "Javaone Keynote\u1234",
+                    Width = 32_000,
+                    Height = 24_000,
                     Size = MediaSize.Large,
                 },
                 new Image
                 {
-                    Uri = "https://apache.org/fory/image/2",
-                    Title = "thumbnail",
+                    Uri = "http://javaone.com/keynote_large.jpg",
+                    Title = string.Empty,
+                    Width = 1_024,
+                    Height = 768,
+                    Size = MediaSize.Large,
+                },
+                new Image
+                {
+                    Uri = "http://javaone.com/keynote_small.jpg",
+                    Title = string.Empty,
                     Width = 320,
-                    Height = 180,
+                    Height = 240,
                     Size = MediaSize.Small,
                 },
             ],
@@ -359,22 +396,10 @@ public static class BenchmarkDataFactory
 
     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,
-                });
+            list.Values.Add(CreateNumericStruct());
         }
 
         return list;
@@ -382,35 +407,10 @@ public static class BenchmarkDataFactory
 
     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,
-            });
+            list.Values.Add(CreateSample());
         }
 
         return list;
@@ -418,39 +418,10 @@ public static class BenchmarkDataFactory
 
     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,
-                    }),
-                ],
-            });
+            list.Values.Add(CreateMediaContent());
         }
 
         return list;
diff --git a/benchmarks/csharp/BenchmarkSerializers.cs b/benchmarks/csharp/BenchmarkSerializers.cs
index 6f6c70393..a648a5711 100644
--- a/benchmarks/csharp/BenchmarkSerializers.cs
+++ b/benchmarks/csharp/BenchmarkSerializers.cs
@@ -35,7 +35,7 @@ internal interface IBenchmarkSerializer<T>
 
 internal sealed class ForySerializer<T> : IBenchmarkSerializer<T>
 {
-    private readonly ForyRuntime _fory = ForyRuntime.Builder().Build();
+    private readonly ForyRuntime _fory = ForyRuntime.Builder().Compatible(true).Build();
 
     public ForySerializer()
     {
@@ -59,16 +59,17 @@ 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);
+        // Keep user type IDs aligned with C++ benchmark registration.
+        fory.Register<NumericStruct>(1);
+        fory.Register<Sample>(2);
+        fory.Register<Media>(3);
+        fory.Register<Image>(4);
+        fory.Register<MediaContent>(5);
+        fory.Register<StructList>(6);
+        fory.Register<SampleList>(7);
+        fory.Register<MediaContentList>(8);
+        fory.Register<Player>(9);
+        fory.Register<MediaSize>(10);
     }
 }
 
@@ -95,18 +96,17 @@ internal sealed class ProtobufSerializer<T> : IBenchmarkSerializer<T>
 
 internal sealed class MessagePackRuntimeSerializer<T> : IBenchmarkSerializer<T>
 {
-    private readonly MessagePackSerializerOptions _options = TypelessContractlessStandardResolver.Options;
+    private readonly MessagePackSerializerOptions _options = StandardResolver.Options;
 
     public string Name => "msgpack";
 
     public byte[] Serialize(T value)
     {
-        return MessagePackSerializer.Typeless.Serialize(value, _options);
+        return MessagePackSerializer.Serialize(value, _options);
     }
 
     public T Deserialize(byte[] payload)
     {
-        object? value = MessagePackSerializer.Typeless.Deserialize(payload, _options);
-        return (T)value!;
+        return MessagePackSerializer.Deserialize<T>(payload, _options);
     }
 }
diff --git a/benchmarks/csharp/README.md b/benchmarks/csharp/README.md
index c57e8c35c..04e72dc3a 100644
--- a/benchmarks/csharp/README.md
+++ b/benchmarks/csharp/README.md
@@ -4,7 +4,7 @@ This benchmark compares Apache Fory C#, protobuf-net, and MessagePack-CSharp.
 
 Serializer setup used in this benchmark:
 
-- `fory`: `Fory.Builder().Build()`
+- `fory`: `Fory.Builder().Compatible(true).Build()`
 - `protobuf`: `protobuf-net` runtime serializer
 - `msgpack`: `MessagePackSerializer.Typeless` with `TypelessContractlessStandardResolver`
 
@@ -23,7 +23,8 @@ cd benchmarks/csharp
 This runs all benchmark cases and generates:
 
 - `build/benchmark_results.json`
-- `report/REPORT.md`
+- `report/README.md`
+- `report/throughput.png` and per-datatype throughput plots (for example `report/struct.png`)
 
 ## Run Options
 
@@ -77,4 +78,5 @@ Latest run (`Darwin arm64`, `.NET 8.0.24`, `--duration 2 --warmup 0.5`):
 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`
+- `benchmarks/csharp/report/README.md`
+- `benchmarks/csharp/report/*.png`
diff --git a/benchmarks/csharp/benchmark_report.py b/benchmarks/csharp/benchmark_report.py
index 8a06c1656..e8e0b26e8 100755
--- a/benchmarks/csharp/benchmark_report.py
+++ b/benchmarks/csharp/benchmark_report.py
@@ -19,19 +19,61 @@
 import argparse
 import json
 import os
+import platform
 from collections import defaultdict
+from datetime import datetime
+
+import matplotlib.pyplot as plt
+import numpy as np
+
+try:
+    import psutil
+
+    HAS_PSUTIL = True
+except ImportError:
+    HAS_PSUTIL = False
+
+COLORS = {
+    "fory": "#FF6f01",
+    "protobuf": "#55BCC2",
+    "msgpack": (0.55, 0.40, 0.45),
+}
+SERIALIZER_ORDER = ["fory", "protobuf", "msgpack"]
+SERIALIZER_LABELS = {
+    "fory": "fory",
+    "protobuf": "protobuf",
+    "msgpack": "msgpack",
+}
+PREFERRED_DATATYPE_ORDER = [
+    "struct",
+    "sample",
+    "mediacontent",
+    "structlist",
+    "samplelist",
+    "mediacontentlist",
+]
+PREFERRED_OPERATION_ORDER = ["serialize", "deserialize"]
 
 
 def parse_args() -> argparse.Namespace:
     parser = argparse.ArgumentParser(
-        description="Generate markdown report from C# benchmark JSON output"
+        description="Plot C# benchmark stats and generate Markdown report"
     )
     parser.add_argument(
         "--json-file",
         default="benchmark_results.json",
         help="Benchmark JSON output file",
     )
-    parser.add_argument("--output-dir", default="report", help="Output directory")
+    parser.add_argument(
+        "--output-dir",
+        default="",
+        help="Output directory for plots and report",
+    )
+    parser.add_argument(
+        "--plot-prefix",
+        default="",
+        help="Image path prefix in Markdown report",
+    )
     return parser.parse_args()
 
 
@@ -40,149 +82,480 @@ def load_results(path: str) -> dict:
         return json.load(f)
 
 
-def format_ops(value: float) -> str:
-    return f"{value:,.0f}"
+def get_system_info(benchmark_data: dict) -> dict:
+    info = {
+        "OS": benchmark_data.get(
+            "OsDescription", f"{platform.system()} {platform.release()}"
+        ),
+        "OS Architecture": benchmark_data.get("OsArchitecture", "Unknown"),
+        "Machine": benchmark_data.get("ProcessArchitecture", platform.machine()),
+        "Runtime Version": benchmark_data.get("RuntimeVersion", "Unknown"),
+        "Benchmark Date (UTC)": benchmark_data.get("GeneratedAtUtc", "Unknown"),
+        "Warmup Seconds": benchmark_data.get("WarmupSeconds", "Unknown"),
+        "Duration Seconds": benchmark_data.get("DurationSeconds", "Unknown"),
+    }
+    processor_count = benchmark_data.get("ProcessorCount")
+    if processor_count is not None:
+        info["CPU Logical Cores (from benchmark)"] = processor_count
+
+    if HAS_PSUTIL:
+        info["CPU Cores (Physical)"] = psutil.cpu_count(logical=False)
+        info["CPU Cores (Logical)"] = psutil.cpu_count(logical=True)
+        info["Total RAM (GB)"] = round(psutil.virtual_memory().total / (1024**3), 2)
+    return info
+
+
+def format_datatype_label(datatype: str) -> str:
+    if datatype.endswith("list"):
+        base = datatype[: -len("list")]
+        if base == "mediacontent":
+            return "MediaContent\nList"
+        return f"{base.capitalize()}\nList"
+    if datatype == "mediacontent":
+        return "MediaContent"
+    return datatype.capitalize()
+
+
+def format_datatype_table_label(datatype: str) -> str:
+    if datatype.endswith("list"):
+        base = datatype[: -len("list")]
+        if base == "mediacontent":
+            return "MediaContentList"
+        return f"{base.capitalize()}List"
+    if datatype == "mediacontent":
+        return "MediaContent"
+    return datatype.capitalize()
+
+
+def format_tps_label(tps: float) -> str:
+    if tps >= 1e9:
+        return f"{tps / 1e9:.2f}G"
+    if tps >= 1e6:
+        return f"{tps / 1e6:.2f}M"
+    if tps >= 1e3:
+        return f"{tps / 1e3:.2f}K"
+    return f"{tps:.0f}"
+
+
+def preferred_ordered_values(values, preferred):
+    return [item for item in preferred if item in values] + sorted(
+        item for item in values if item not in preferred
+    )
 
 
-def format_ns(value: float) -> str:
-    return f"{value:,.1f}"
+def process_benchmark_rows(rows):
+    raw_timings = defaultdict(lambda: defaultdict(lambda: defaultdict(list)))
+    raw_throughputs = defaultdict(lambda: defaultdict(lambda: defaultdict(list)))
+    raw_sizes = defaultdict(lambda: defaultdict(list))
+
+    for row in rows:
+        serializer = str(row.get("Serializer", "")).lower()
+        data_type = str(row.get("DataType", "")).lower()
+        operation = str(row.get("Operation", "")).lower()
+        if not serializer or not data_type or not operation:
+            continue
+
+        avg_ns_raw = row.get("AverageNanoseconds")
+        ops_raw = row.get("OperationsPerSecond")
+        avg_ns = float(avg_ns_raw) if avg_ns_raw is not None else 0.0
+        ops = float(ops_raw) if ops_raw is not None else 0.0
+
+        if avg_ns <= 0.0 and ops > 0.0:
+            avg_ns = 1e9 / ops
+        if ops <= 0.0 and avg_ns > 0.0:
+            ops = 1e9 / avg_ns
+
+        if avg_ns > 0.0:
+            raw_timings[data_type][operation][serializer].append(avg_ns)
+        if ops > 0.0:
+            raw_throughputs[data_type][operation][serializer].append(ops)
+
+        serialized_size = row.get("SerializedSize")
+        if serialized_size is not None:
+            raw_sizes[data_type][serializer].append(int(round(serialized_size)))
+
+    timings = defaultdict(lambda: defaultdict(dict))
+    throughputs = defaultdict(lambda: defaultdict(dict))
+    sizes = defaultdict(dict)
+
+    for data_type, op_values in raw_timings.items():
+        for operation, serializer_values in op_values.items():
+            for serializer, values in serializer_values.items():
+                timings[data_type][operation][serializer] = sum(values) / len(values)
+
+    for data_type, op_values in raw_throughputs.items():
+        for operation, serializer_values in op_values.items():
+            for serializer, values in serializer_values.items():
+                throughputs[data_type][operation][serializer] = sum(values) / len(
+                    values
+                )
+
+    for data_type, serializer_values in raw_sizes.items():
+        for serializer, values in serializer_values.items():
+            sizes[data_type][serializer] = sum(values) / len(values)
+
+    return timings, throughputs, sizes
+
+
+def build_coverage(rows):
+    cases = set()
+    serializers = set()
+    datatypes = set()
+    operations = set()
+    for row in rows:
+        serializer = str(row.get("Serializer", "")).lower()
+        data_type = str(row.get("DataType", "")).lower()
+        operation = str(row.get("Operation", "")).lower()
+        if not serializer or not data_type or not operation:
+            continue
+        cases.add((serializer, data_type, operation))
+        serializers.add(serializer)
+        datatypes.add(data_type)
+        operations.add(operation)
+
+    expected_cases = (
+        len(SERIALIZER_ORDER)
+        * len(PREFERRED_DATATYPE_ORDER)
+        * len(PREFERRED_OPERATION_ORDER)
+    )
+    return {
+        "case_count": len(cases),
+        "expected_case_count": expected_cases,
+        "serializers": sorted(serializers),
+        "datatypes": preferred_ordered_values(
+            list(datatypes), PREFERRED_DATATYPE_ORDER
+        ),
+        "operations": preferred_ordered_values(
+            list(operations), PREFERRED_OPERATION_ORDER
+        ),
+        "is_partial": len(cases) < expected_cases,
+    }
 
 
-def format_size(value: float) -> str:
-    return str(int(round(value)))
+def plot_datatype(ax, throughputs: dict, datatype: str, operation: str) -> None:
+    if datatype not in throughputs or operation not in throughputs[datatype]:
+        ax.set_title(f"{datatype} {operation} - No Data")
+        ax.axis("off")
+        return
+
+    libs = set(throughputs[datatype][operation].keys())
+    lib_order = [lib for lib in SERIALIZER_ORDER if lib in libs]
+    if not lib_order:
+        ax.set_title(f"{datatype} {operation} - No Supported Serializer Data")
+        ax.axis("off")
+        return
+    throughput = [throughputs[datatype][operation].get(lib, 0) for lib in lib_order]
+    colors = [COLORS.get(lib, "#888888") for lib in lib_order]
+
+    x = np.arange(len(lib_order))
+    bars = ax.bar(x, throughput, color=colors, width=0.6)
+
+    ax.set_title(f"{operation.capitalize()} Throughput (higher is better)")
+    ax.set_xticks(x)
+    ax.set_xticklabels([SERIALIZER_LABELS.get(lib, lib) for lib in lib_order])
+    ax.set_ylabel("Throughput (ops/sec)")
+    ax.grid(True, axis="y", linestyle="--", alpha=0.5)
+    ax.ticklabel_format(style="scientific", axis="y", scilimits=(0, 0))
+
+    for bar, tps_value in zip(bars, throughput):
+        height = bar.get_height()
+        ax.annotate(
+            format_tps_label(tps_value),
+            xy=(bar.get_x() + bar.get_width() / 2, height),
+            xytext=(0, 3),
+            textcoords="offset points",
+            ha="center",
+            va="bottom",
+            fontsize=9,
+        )
 
 
-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
+def plot_combined_tps_subplot(ax, throughputs, grouped_datatypes, operation, title):
+    if not grouped_datatypes:
+        ax.set_title(f"{title}\nNo Data")
+        ax.axis("off")
+        return
+
+    x = np.arange(len(grouped_datatypes))
+    available_libs = [
+        lib
+        for lib in SERIALIZER_ORDER
+        if any(throughputs[dt][operation].get(lib, 0) > 0 for dt in grouped_datatypes)
+    ]
+    if not available_libs:
+        ax.set_title(f"{title}\nNo Data")
+        ax.axis("off")
+        return
+
+    width = 0.8 / len(available_libs)
+    for idx, lib in enumerate(available_libs):
+        tps = [throughputs[dt][operation].get(lib, 0) for dt in grouped_datatypes]
+        offset = (idx - (len(available_libs) - 1) / 2) * width
+        ax.bar(
+            x + offset,
+            tps,
+            width,
+            label=SERIALIZER_LABELS.get(lib, lib),
+            color=COLORS.get(lib, "#888888"),
         )
-        best = rows[0]
 
-        lines.append(f"### `{data_type}` / `{operation}`")
-        lines.append("")
-        lines.append("| Serializer | Ops/sec | ns/op | Relative to best |")
-        lines.append("| ---------- | ------: | ----: | ---------------: |")
+    ax.set_title(title)
+    ax.set_xticks(x)
+    ax.set_xticklabels([format_datatype_label(dt) for dt in grouped_datatypes])
+    ax.legend()
+    ax.grid(True, axis="y", linestyle="--", alpha=0.5)
+    ax.ticklabel_format(style="scientific", axis="y", scilimits=(0, 0))
+
+
+def build_markdown(
+    args,
+    system_info,
+    coverage,
+    timings,
+    throughputs,
+    sizes,
+    datatypes,
+    operations,
+    plot_images,
+):
+    report_lines = [
+        "# C# Benchmark Performance Report\n\n",
+        f"_Generated on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}_\n\n",
+        "## How to Generate This Report\n\n",
+        "```bash\n",
+        "cd benchmarks/csharp\n",
+        "dotnet run -c Release --project ./Fory.CSharpBenchmark.csproj -- --output build/benchmark_results.json\n",
+        "python3 benchmark_report.py --json-file build/benchmark_results.json --output-dir report\n",
+        "```\n\n",
+        "## Hardware & OS Info\n\n",
+        "| Key | Value |\n",
+        "|-----|-------|\n",
+    ]
+
+    for key, value in system_info.items():
+        report_lines.append(f"| {key} | {value} |\n")
+
+    report_lines.append("\n## Benchmark Coverage\n\n")
+    report_lines.append("| Key | Value |\n")
+    report_lines.append("|-----|-------|\n")
+    report_lines.append(
+        f"| Cases in input JSON | {coverage['case_count']} / {coverage['expected_case_count']} |\n"
+    )
+    report_lines.append(
+        f"| Serializers | {', '.join(coverage['serializers']) or 'N/A'} |\n"
+    )
+    report_lines.append(
+        f"| Datatypes | {', '.join(coverage['datatypes']) or 'N/A'} |\n"
+    )
+    report_lines.append(
+        f"| Operations | {', '.join(coverage['operations']) or 'N/A'} |\n"
+    )
+    if coverage["is_partial"]:
+        report_lines.append(
+            "\n> Warning: benchmark input is partial; plots/tables only show available cases.\n"
+        )
 
-        for row in rows:
-            relative = best["OperationsPerSecond"] / row["OperationsPerSecond"]
-            lines.append(
+    report_lines.append("\n## Benchmark Plots\n")
+    report_lines.append("\nAll class-level plots below show throughput (ops/sec).\n")
+    plot_images_sorted = sorted(
+        plot_images, key=lambda item: (0 if item[0] == "throughput" else 1, item[0])
+    )
+    for datatype, img in plot_images_sorted:
+        img_filename = os.path.basename(img)
+        img_path_report = args.plot_prefix + img_filename
+        report_lines.append(f"\n### {datatype.replace('_', ' ').title()}\n\n")
+        report_lines.append(
+            f'<p align="center">\n<img src="{img_path_report}" width="90%" />\n</p>\n'
+        )
+
+    report_lines.append("\n## Benchmark Results\n\n")
+    report_lines.append("### Timing Results (nanoseconds)\n\n")
+    report_lines.append(
+        "| Datatype | Operation | fory (ns) | protobuf (ns) | msgpack (ns) | Fastest |\n"
+    )
+    report_lines.append(
+        "|----------|-----------|-----------|---------------|--------------|---------|\n"
+    )
+
+    for datatype in datatypes:
+        for operation in operations:
+            times = {
+                lib: timings[datatype][operation].get(lib, 0)
+                for lib in SERIALIZER_ORDER
+            }
+            positive_times = {lib: t for lib, t in times.items() if t > 0}
+            fastest = "N/A"
+            if positive_times:
+                fastest_lib = min(positive_times, key=positive_times.get)
+                fastest = SERIALIZER_LABELS.get(fastest_lib, fastest_lib)
+            report_lines.append(
                 "| "
-                f"{row['Serializer']}"
-                " | "
-                f"{format_ops(row['OperationsPerSecond'])}"
-                " | "
-                f"{format_ns(row['AverageNanoseconds'])}"
-                " | "
-                f"{relative:.2f}x"
-                " |"
+                + f"{format_datatype_table_label(datatype)} | {operation.capitalize()} | "
+                + " | ".join(
+                    f"{times[lib]:.1f}" if times[lib] > 0 else "N/A"
+                    for lib in SERIALIZER_ORDER
+                )
+                + f" | {fastest} |\n"
             )
 
-        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)
+    report_lines.append("\n### Throughput Results (ops/sec)\n\n")
+    report_lines.append(
+        "| Datatype | Operation | fory TPS | protobuf TPS | msgpack TPS | Fastest |\n"
+    )
+    report_lines.append(
+        "|----------|-----------|----------|--------------|-------------|---------|\n"
+    )
+
+    for datatype in datatypes:
+        for operation in operations:
+            tps_values = {
+                lib: throughputs[datatype][operation].get(lib, 0)
+                for lib in SERIALIZER_ORDER
+            }
+            positive_tps = {lib: v for lib, v in tps_values.items() if v > 0}
+            fastest = "N/A"
+            if positive_tps:
+                fastest_lib = max(positive_tps, key=positive_tps.get)
+                fastest = SERIALIZER_LABELS.get(fastest_lib, fastest_lib)
+            report_lines.append(
+                "| "
+                + f"{format_datatype_table_label(datatype)} | {operation.capitalize()} | "
+                + " | ".join(
+                    f"{tps_values[lib]:,.0f}" if tps_values[lib] > 0 else "N/A"
+                    for lib in SERIALIZER_ORDER
+                )
+                + f" | {fastest} |\n"
+            )
+
+    if sizes:
+        report_lines.append("\n### Serialized Data Sizes (bytes)\n\n")
+        report_lines.append("| Datatype | fory | protobuf | msgpack |\n")
+        report_lines.append("|----------|------|----------|---------|\n")
+        for datatype in datatypes:
+            row_values = []
+            has_value = False
+            for serializer in SERIALIZER_ORDER:
+                size = sizes[datatype].get(serializer)
+                if size is None:
+                    row_values.append("N/A")
+                else:
+                    row_values.append(str(int(round(size))))
+                    has_value = True
+            if has_value:
+                report_lines.append(
+                    f"| {format_datatype_table_label(datatype)} | "
+                    + " | ".join(row_values)
+                    + " |\n"
+                )
+
+    return "".join(report_lines)
 
 
 def main() -> None:
     args = parse_args()
-    result = load_results(args.json_file)
+    benchmark_data = load_results(args.json_file)
+
+    if args.output_dir.strip():
+        output_dir = args.output_dir
+    else:
+        output_dir = datetime.now().strftime("%Y_%m_%d_%H_%M_%S")
+    os.makedirs(output_dir, exist_ok=True)
 
-    os.makedirs(args.output_dir, exist_ok=True)
-    report_path = os.path.join(args.output_dir, "REPORT.md")
+    rows = benchmark_data.get("Results", [])
+    timings, throughputs, sizes = process_benchmark_rows(rows)
+    coverage = build_coverage(rows)
+
+    datatype_candidates = (
+        set(timings.keys()) | set(throughputs.keys()) | set(sizes.keys())
+    )
+    datatypes = preferred_ordered_values(
+        list(datatype_candidates), PREFERRED_DATATYPE_ORDER
+    )
+    operations_present = set()
+    for datatype in datatypes:
+        operations_present.update(timings[datatype].keys())
+        operations_present.update(throughputs[datatype].keys())
+    operations = preferred_ordered_values(
+        list(operations_present), PREFERRED_OPERATION_ORDER
+    )
 
-    report = build_report(result)
+    plot_images = []
+    for datatype in datatypes:
+        fig, axes = plt.subplots(1, 2, figsize=(12, 5))
+        for index, operation in enumerate(PREFERRED_OPERATION_ORDER):
+            plot_datatype(axes[index], throughputs, datatype, operation)
+        fig.suptitle(f"{format_datatype_table_label(datatype)} Throughput", fontsize=14)
+        fig.tight_layout(rect=[0, 0, 1, 0.95])
+        plot_path = os.path.join(output_dir, f"{datatype}.png")
+        plt.savefig(plot_path, dpi=150)
+        plot_images.append((datatype, plot_path))
+        plt.close()
+
+    non_list_datatypes = [dt for dt in datatypes if not dt.endswith("list")]
+    list_datatypes = [dt for dt in datatypes if dt.endswith("list")]
+    fig, axes = plt.subplots(1, 4, figsize=(28, 6))
+    combined_subplots = [
+        (
+            axes[0],
+            non_list_datatypes,
+            "serialize",
+            "Serialize Throughput (higher is better)",
+        ),
+        (
+            axes[1],
+            non_list_datatypes,
+            "deserialize",
+            "Deserialize Throughput (higher is better)",
+        ),
+        (
+            axes[2],
+            list_datatypes,
+            "serialize",
+            "Serialize Throughput (*List, higher is better)",
+        ),
+        (
+            axes[3],
+            list_datatypes,
+            "deserialize",
+            "Deserialize Throughput (*List, higher is better)",
+        ),
+    ]
+    for ax, grouped_datatypes, operation, title in combined_subplots:
+        plot_combined_tps_subplot(ax, throughputs, grouped_datatypes, operation, title)
+    # Keep the y-label style consistent with single-datatype plots (axis label,
+    # not figure-level label) and avoid overlap with y-ticks.
+    axes[0].set_ylabel("Throughput (ops/sec)")
+    fig.tight_layout()
+    throughput_path = os.path.join(output_dir, "throughput.png")
+    plt.savefig(throughput_path, dpi=150)
+    plot_images.append(("throughput", throughput_path))
+    plt.close()
+
+    report = build_markdown(
+        args=args,
+        system_info=get_system_info(benchmark_data),
+        coverage=coverage,
+        timings=timings,
+        throughputs=throughputs,
+        sizes=sizes,
+        datatypes=datatypes,
+        operations=operations,
+        plot_images=plot_images,
+    )
+    legacy_report_path = os.path.join(output_dir, "REPORT.md")
+    if os.path.exists(legacy_report_path):
+        os.remove(legacy_report_path)
+    report_path = os.path.join(output_dir, "README.md")
     with open(report_path, "w", encoding="utf-8") as f:
         f.write(report)
 
-    print(f"Report written to {report_path}")
+    if coverage["is_partial"]:
+        print(
+            "Warning: partial benchmark input detected "
+            f"({coverage['case_count']}/{coverage['expected_case_count']} cases)."
+        )
+    print(f"Plots saved in: {output_dir}")
+    print(f"Markdown report generated at: {report_path}")
 
 
 if __name__ == "__main__":
diff --git a/benchmarks/csharp/run.sh b/benchmarks/csharp/run.sh
index fd95d4fb6..fa436553a 100755
--- a/benchmarks/csharp/run.sh
+++ b/benchmarks/csharp/run.sh
@@ -31,6 +31,7 @@ DATA=""
 SERIALIZER=""
 DURATION="3"
 WARMUP="1"
+OUTPUT_DIR=""
 
 usage() {
     cat <<USAGE
@@ -45,6 +46,7 @@ Options:
                                Filter benchmark by serializer
   --duration <seconds>         Measure duration per benchmark (default: 3)
   --warmup <seconds>           Warmup duration per benchmark (default: 1)
+  --output-dir <dir>           Base directory for benchmark outputs
   --help                       Show this help
 USAGE
     exit 0
@@ -68,6 +70,10 @@ while [[ $# -gt 0 ]]; do
             WARMUP="$2"
             shift 2
             ;;
+        --output-dir)
+            OUTPUT_DIR="$2"
+            shift 2
+            ;;
         --help|-h)
             usage
             ;;
@@ -78,8 +84,16 @@ while [[ $# -gt 0 ]]; do
     esac
 done
 
-mkdir -p build report
-RESULT_JSON="build/benchmark_results.json"
+if [[ -n "$OUTPUT_DIR" ]]; then
+    BUILD_DIR="$OUTPUT_DIR/build"
+    REPORT_DIR="$OUTPUT_DIR/report"
+else
+    BUILD_DIR="build"
+    REPORT_DIR="report"
+fi
+
+mkdir -p "$BUILD_DIR" "$REPORT_DIR"
+RESULT_JSON="$BUILD_DIR/benchmark_results.json"
 
 RUN_ARGS=(
     --output "$RESULT_JSON"
@@ -105,8 +119,22 @@ 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
+# Check for Python dependencies needed for plotting.
+if ! python3 -c "import matplotlib" 2>/dev/null; then
+    echo -e "${YELLOW}Installing required Python packages...${NC}"
+    pip3 install matplotlib numpy psutil
+fi
+
+python3 benchmark_report.py --json-file "$RESULT_JSON" --output-dir "$REPORT_DIR"
 
 echo ""
 echo -e "${GREEN}=== All done! ===${NC}"
-echo "Report generated at: $SCRIPT_DIR/report/REPORT.md"
+if [[ "$REPORT_DIR" = /* ]]; then
+    REPORT_PATH="$REPORT_DIR/README.md"
+    REPORT_PLOTS_DIR="$REPORT_DIR"
+else
+    REPORT_PATH="$SCRIPT_DIR/$REPORT_DIR/README.md"
+    REPORT_PLOTS_DIR="$SCRIPT_DIR/$REPORT_DIR"
+fi
+echo "Report generated at: $REPORT_PATH"
+echo "Plots saved in: $REPORT_PLOTS_DIR/"
diff --git a/csharp/src/Fory.Generator/ForyObjectGenerator.cs b/csharp/src/Fory.Generator/ForyObjectGenerator.cs
index ea8e76244..28d678ede 100644
--- a/csharp/src/Fory.Generator/ForyObjectGenerator.cs
+++ b/csharp/src/Fory.Generator/ForyObjectGenerator.cs
@@ -136,16 +136,24 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
     private static void EmitObjectSerializer(StringBuilder sb, TypeModel model)
     {
         sb.AppendLine(
-            $"file sealed class {model.SerializerName} : global::Apache.Fory.Serializer<{model.TypeName}>");
+            $"file sealed class {model.SerializerName} : global::Apache.Fory.Serializer<{model.TypeName}>, global::Apache.Fory.ICompatibleNoTypeMetaReader<{model.TypeName}>");
         sb.AppendLine("{");
-        foreach (MemberModel member in model.Members.Where(m => m.UseDictionaryTypeInfoCache))
-        {
-            string cacheId = Sanitize(member.Name);
-            sb.AppendLine($"    private global::System.Type? __Fory{cacheId}DictRuntimeType;");
-            sb.AppendLine($"    private global::Apache.Fory.TypeInfo? __Fory{cacheId}DictTypeInfo;");
-        }
+        sb.AppendLine("    private static readonly object __ForyCompatibleSchemaCacheLock = new();");
+        sb.AppendLine("    private static ulong __ForyCompatibleSchemaResolverVersion;");
+        sb.AppendLine("    private static ulong __ForyCompatibleSchemaHashNoTrackRef;");
+        sb.AppendLine("    private static ulong __ForyCompatibleSchemaHashTrackRef;");
+        sb.AppendLine("    private static global::Apache.Fory.TypeMeta? __ForyLastCompatibleSchemaTypeMetaNoTrackRef;");
+        sb.AppendLine("    private static bool __ForyLastCompatibleSchemaMatchedNoTrackRef;");
+        sb.AppendLine("    private static global::Apache.Fory.TypeMeta? __ForyLastCompatibleSchemaTypeMetaTrackRef;");
+        sb.AppendLine("    private static bool __ForyLastCompatibleSchemaMatchedTrackRef;");
+        sb.AppendLine(
+            $"    private const bool __ForyAllFieldsBuiltIn = {BoolLiteral(model.SortedMembers.All(m => m.DynamicAnyKind == DynamicAnyKind.None && m.Classification.IsBuiltIn))};");
+        sb.AppendLine(
+            "    private static global::System.Collections.Generic.IReadOnlyList<global::Apache.Fory.TypeMetaFieldInfo>? __ForyCompatibleFieldsNoTrackRef;");
+        sb.AppendLine(
+            "    private static global::System.Collections.Generic.IReadOnlyList<global::Apache.Fory.TypeMetaFieldInfo>? __ForyCompatibleFieldsTrackRef;");
 
-        if (model.Members.Any(m => m.UseDictionaryTypeInfoCache))
+        if (model.SortedMembers.Length > 0)
         {
             sb.AppendLine();
         }
@@ -160,21 +168,6 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
         sb.AppendLine("        return nullable ? global::Apache.Fory.RefMode.NullOnly : global::Apache.Fory.RefMode.None;");
         sb.AppendLine("    }");
         sb.AppendLine();
-        sb.AppendLine("    private static bool __ForyNeedsTypeInfoForField(global::Apache.Fory.TypeId typeId)");
-        sb.AppendLine("    {");
-        sb.AppendLine("        return typeId switch");
-        sb.AppendLine("        {");
-        sb.AppendLine("            global::Apache.Fory.TypeId.Struct or");
-        sb.AppendLine("            global::Apache.Fory.TypeId.CompatibleStruct or");
-        sb.AppendLine("            global::Apache.Fory.TypeId.NamedStruct or");
-        sb.AppendLine("            global::Apache.Fory.TypeId.NamedCompatibleStruct or");
-        sb.AppendLine("            global::Apache.Fory.TypeId.Ext or");
-        sb.AppendLine("            global::Apache.Fory.TypeId.NamedExt or");
-        sb.AppendLine("            global::Apache.Fory.TypeId.Unknown => true,");
-        sb.AppendLine("            _ => false,");
-        sb.AppendLine("        };");
-        sb.AppendLine("    }");
-        sb.AppendLine();
         sb.AppendLine("    private static bool __ForyCanReadCompatiblePrimitive(global::Apache.Fory.TypeId typeId)");
         sb.AppendLine("    {");
         sb.AppendLine("        return typeId switch");
@@ -287,6 +280,135 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
         sb.AppendLine("        return context.TypeResolver.GetSerializer<T>().Read(context, refMode, readTypeInfo);");
         sb.AppendLine("    }");
         sb.AppendLine();
+        sb.AppendLine(
+            "    private static global::System.Collections.Generic.IReadOnlyList<global::Apache.Fory.TypeMetaFieldInfo> __ForyBuildCompatibleTypeMetaFields(bool trackRef)");
+        sb.AppendLine("    {");
+        if (model.SortedMembers.Length == 0)
+        {
+            sb.AppendLine("        return global::System.Array.Empty<global::Apache.Fory.TypeMetaFieldInfo>();");
+        }
+        else
+        {
+            sb.AppendLine("        return new global::Apache.Fory.TypeMetaFieldInfo[]");
+            sb.AppendLine("        {");
+            foreach (MemberModel member in model.SortedMembers)
+            {
+                sb.AppendLine(
+                    $"            new global::Apache.Fory.TypeMetaFieldInfo({BuildTypeMetaFieldIdExpression(member.FieldId)}, \"{EscapeString(member.FieldIdentifier)}\", {BuildCompatibleTypeMetaExpression(member.TypeMeta, "trackRef")}),");
+            }
+
+            sb.AppendLine("        };");
+        }
+
+        sb.AppendLine("    }");
+        sb.AppendLine();
+        sb.AppendLine(
+            "    private bool __ForyVerifyExactCompatibleSchema(global::Apache.Fory.TypeMeta typeMeta, bool trackRef)");
+        sb.AppendLine("    {");
+        sb.AppendLine(
+            "        global::System.Collections.Generic.IReadOnlyList<global::Apache.Fory.TypeMetaFieldInfo> expectedFields = CompatibleTypeMetaFields(trackRef);");
+        sb.AppendLine("        if (typeMeta.Fields.Count != expectedFields.Count)");
+        sb.AppendLine("        {");
+        sb.AppendLine("            return false;");
+        sb.AppendLine("        }");
+        sb.AppendLine();
+        sb.AppendLine("        for (int i = 0; i < expectedFields.Count; i++)");
+        sb.AppendLine("        {");
+        sb.AppendLine(
+            "            global::Apache.Fory.TypeMetaFieldInfo remoteField = typeMeta.Fields[i];");
+        sb.AppendLine(
+            "            global::Apache.Fory.TypeMetaFieldInfo localField = expectedFields[i];");
+        sb.AppendLine("            if (remoteField.FieldId.HasValue && localField.FieldId.HasValue)");
+        sb.AppendLine("            {");
+        sb.AppendLine(
+            "                if (remoteField.FieldId.Value != localField.FieldId.Value || !remoteField.FieldType.Equals(localField.FieldType))");
+        sb.AppendLine("                {");
+        sb.AppendLine("                    return false;");
+        sb.AppendLine("                }");
+        sb.AppendLine();
+        sb.AppendLine("                continue;");
+        sb.AppendLine("            }");
+        sb.AppendLine(
+            "            if (remoteField.FieldName != localField.FieldName || !remoteField.FieldType.Equals(localField.FieldType))");
+        sb.AppendLine("            {");
+        sb.AppendLine("                return false;");
+        sb.AppendLine("            }");
+        sb.AppendLine("        }");
+        sb.AppendLine();
+        sb.AppendLine("        return true;");
+        sb.AppendLine("    }");
+        sb.AppendLine();
+        sb.AppendLine(
+            "    private static void __ForyEnsureCompatibleSchemaCache(global::Apache.Fory.TypeResolver typeResolver)");
+        sb.AppendLine("    {");
+        sb.AppendLine("        ulong resolverVersion = typeResolver.VersionHash();");
+        sb.AppendLine("        if (__ForyCompatibleSchemaResolverVersion == resolverVersion)");
+        sb.AppendLine("        {");
+        sb.AppendLine("            return;");
+        sb.AppendLine("        }");
+        sb.AppendLine();
+        sb.AppendLine("        lock (__ForyCompatibleSchemaCacheLock)");
+        sb.AppendLine("        {");
+        sb.AppendLine("            if (__ForyCompatibleSchemaResolverVersion == resolverVersion)");
+        sb.AppendLine("            {");
+        sb.AppendLine("                return;");
+        sb.AppendLine("            }");
+        sb.AppendLine();
+        sb.AppendLine(
+            $"            __ForyCompatibleSchemaHashNoTrackRef = typeResolver.GetCompatibleTypeMetaHeaderHash(typeof({model.TypeName}), false);");
+        sb.AppendLine(
+            $"            __ForyCompatibleSchemaHashTrackRef = typeResolver.GetCompatibleTypeMetaHeaderHash(typeof({model.TypeName}), true);");
+        sb.AppendLine("            __ForyLastCompatibleSchemaTypeMetaNoTrackRef = null;");
+        sb.AppendLine("            __ForyLastCompatibleSchemaMatchedNoTrackRef = false;");
+        sb.AppendLine("            __ForyLastCompatibleSchemaTypeMetaTrackRef = null;");
+        sb.AppendLine("            __ForyLastCompatibleSchemaMatchedTrackRef = false;");
+        sb.AppendLine("            __ForyCompatibleSchemaResolverVersion = resolverVersion;");
+        sb.AppendLine("        }");
+        sb.AppendLine("    }");
+        sb.AppendLine();
+        sb.AppendLine(
+            "    private bool __ForyCachedExactCompatibleSchema(global::Apache.Fory.TypeMeta typeMeta, bool trackRef, global::Apache.Fory.TypeResolver typeResolver)");
+        sb.AppendLine("    {");
+        sb.AppendLine("        if (trackRef)");
+        sb.AppendLine("        {");
+        sb.AppendLine(
+            "            if (global::System.Object.ReferenceEquals(__ForyLastCompatibleSchemaTypeMetaTrackRef, typeMeta))");
+        sb.AppendLine("            {");
+        sb.AppendLine("                return __ForyLastCompatibleSchemaMatchedTrackRef;");
+        sb.AppendLine("            }");
+        sb.AppendLine();
+        sb.AppendLine("            __ForyEnsureCompatibleSchemaCache(typeResolver);");
+        sb.AppendLine();
+        sb.AppendLine("            bool matched = false;");
+        sb.AppendLine("            if (typeMeta.HeaderHash == __ForyCompatibleSchemaHashTrackRef)");
+        sb.AppendLine("            {");
+        sb.AppendLine("                matched = __ForyVerifyExactCompatibleSchema(typeMeta, true);");
+        sb.AppendLine("            }");
+        sb.AppendLine();
+        sb.AppendLine("            __ForyLastCompatibleSchemaTypeMetaTrackRef = typeMeta;");
+        sb.AppendLine("            __ForyLastCompatibleSchemaMatchedTrackRef = matched;");
+        sb.AppendLine("            return matched;");
+        sb.AppendLine("        }");
+        sb.AppendLine();
+        sb.AppendLine(
+            "        if (global::System.Object.ReferenceEquals(__ForyLastCompatibleSchemaTypeMetaNoTrackRef, typeMeta))");
+        sb.AppendLine("        {");
+        sb.AppendLine("            return __ForyLastCompatibleSchemaMatchedNoTrackRef;");
+        sb.AppendLine("        }");
+        sb.AppendLine();
+        sb.AppendLine("        __ForyEnsureCompatibleSchemaCache(typeResolver);");
+        sb.AppendLine();
+        sb.AppendLine("        bool noTrackMatched = false;");
+        sb.AppendLine("        if (typeMeta.HeaderHash == __ForyCompatibleSchemaHashNoTrackRef)");
+        sb.AppendLine("        {");
+        sb.AppendLine("            noTrackMatched = __ForyVerifyExactCompatibleSchema(typeMeta, false);");
+        sb.AppendLine("        }");
+        sb.AppendLine();
+        sb.AppendLine("        __ForyLastCompatibleSchemaTypeMetaNoTrackRef = typeMeta;");
+        sb.AppendLine("        __ForyLastCompatibleSchemaMatchedNoTrackRef = noTrackMatched;");
+        sb.AppendLine("        return noTrackMatched;");
+        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)");
@@ -319,23 +441,19 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
         sb.AppendLine();
         sb.AppendLine("    public global::System.Collections.Generic.IReadOnlyList<global::Apache.Fory.TypeMetaFieldInfo> CompatibleTypeMetaFields(bool trackRef)");
         sb.AppendLine("    {");
-        if (model.SortedMembers.Length == 0)
-        {
-            sb.AppendLine("        return global::System.Array.Empty<global::Apache.Fory.TypeMetaFieldInfo>();");
-        }
-        else
-        {
-            sb.AppendLine("        return new global::Apache.Fory.TypeMetaFieldInfo[]");
-            sb.AppendLine("        {");
-            foreach (MemberModel member in model.SortedMembers)
-            {
-                sb.AppendLine(
-                    $"            new global::Apache.Fory.TypeMetaFieldInfo(null, \"{EscapeString(member.Name)}\", {BuildCompatibleTypeMetaExpression(member.TypeMeta, "trackRef")}),");
-            }
-
-            sb.AppendLine("        };");
-        }
-
+        sb.AppendLine("        if (trackRef)");
+        sb.AppendLine("        {");
+        sb.AppendLine(
+            "            return __ForyCompatibleFieldsTrackRef ??= __ForyBuildCompatibleTypeMetaFields(true);");
+        sb.AppendLine("        }");
+        sb.AppendLine();
+        sb.AppendLine(
+            "        return __ForyCompatibleFieldsNoTrackRef ??= __ForyBuildCompatibleTypeMetaFields(false);");
+        sb.AppendLine("    }");
+        sb.AppendLine();
+        sb.AppendLine("    public bool SupportsCompatibleReadWithoutTypeMeta()");
+        sb.AppendLine("    {");
+        sb.AppendLine("        return false;");
         sb.AppendLine("    }");
         sb.AppendLine();
         sb.AppendLine(
@@ -367,30 +485,106 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
             EmitWriteMember(sb, member, false);
         }
 
+        sb.AppendLine("    }");
+        sb.AppendLine();
+        sb.AppendLine($"    public {model.TypeName} ReadDataCompatibleNoTypeMeta(global::Apache.Fory.ReadContext context)");
+        sb.AppendLine("    {");
+        sb.AppendLine($"        {model.TypeName} valueNoTypeMeta = new {model.TypeName}();");
+        if (model.Kind == DeclKind.Class)
+        {
+            sb.AppendLine("        context.RefReader.BindPendingReference(valueNoTypeMeta);");
+        }
+
+        foreach (MemberModel member in model.SortedMembers)
+        {
+            EmitReadMemberAssignment(
+                sb,
+                member,
+                BuildWriteRefModeExpression(member),
+                BuildCompatibleReadTypeInfoExpression(member),
+                "valueNoTypeMeta",
+                "CompatNoTypeMeta",
+                4,
+                true);
+        }
+
+        sb.AppendLine("        return valueNoTypeMeta;");
         sb.AppendLine("    }");
         sb.AppendLine();
         sb.AppendLine($"    public override {model.TypeName} ReadData(global::Apache.Fory.ReadContext context)");
         sb.AppendLine("    {");
         sb.AppendLine("        if (context.Compatible)");
         sb.AppendLine("        {");
-        sb.AppendLine($"            global::Apache.Fory.TypeMeta typeMeta = context.ConsumeCompatibleTypeMeta(typeof({model.TypeName}));");
+        sb.AppendLine(
+            $"            if (!context.TryConsumeCompatibleTypeMeta(typeof({model.TypeName}), out global::Apache.Fory.TypeMeta? maybeTypeMeta) || maybeTypeMeta is null)");
+        sb.AppendLine("            {");
+        sb.AppendLine("                return ReadDataCompatibleNoTypeMeta(context);");
+        sb.AppendLine("            }");
+        sb.AppendLine();
+        sb.AppendLine("            global::Apache.Fory.TypeMeta typeMeta = maybeTypeMeta;");
         sb.AppendLine($"            {model.TypeName} value = new {model.TypeName}();");
         if (model.Kind == DeclKind.Class)
         {
             sb.AppendLine("            context.RefReader.BindPendingReference(value);");
         }
 
-        sb.AppendLine("            foreach (global::Apache.Fory.TypeMetaFieldInfo remoteField in typeMeta.Fields)");
+        sb.AppendLine("            bool __ForyExactCompatibleSchema = __ForyCachedExactCompatibleSchema(typeMeta, context.TrackRef, context.TypeResolver);");
+        sb.AppendLine("            if (__ForyAllFieldsBuiltIn && __ForyExactCompatibleSchema)");
         sb.AppendLine("            {");
+        foreach (MemberModel member in model.SortedMembers)
+        {
+            EmitReadMemberAssignment(
+                sb,
+                member,
+                BuildWriteRefModeExpression(member),
+                "false",
+                "value",
+                "CompatExact",
+                6,
+                true);
+        }
+
+        sb.AppendLine("                return value;");
+        sb.AppendLine("            }");
+        sb.AppendLine();
+        sb.AppendLine("            if (__ForyExactCompatibleSchema)");
+        sb.AppendLine("            {");
+        foreach (MemberModel member in model.SortedMembers)
+        {
+            EmitReadMemberAssignment(
+                sb,
+                member,
+                BuildWriteRefModeExpression(member),
+                BuildCompatibleReadTypeInfoExpression(member),
+                "value",
+                "CompatExactTyped",
+                6,
+                true);
+        }
+
+        sb.AppendLine("                return value;");
+        sb.AppendLine("            }");
+        sb.AppendLine();
+        sb.AppendLine("            for (int i = 0; i < typeMeta.Fields.Count; i++)");
+        sb.AppendLine("            {");
+        sb.AppendLine("                global::Apache.Fory.TypeMetaFieldInfo remoteField = typeMeta.Fields[i];");
         sb.AppendLine("                global::Apache.Fory.RefMode remoteRefMode = __ForyRefMode(remoteField.FieldType.Nullable, remoteField.FieldType.TrackRef);");
-        sb.AppendLine("                bool remoteReadTypeInfo = __ForyNeedsTypeInfoForField((global::Apache.Fory.TypeId)remoteField.FieldType.TypeId);");
-        sb.AppendLine("                switch (remoteField.FieldName)");
+        sb.AppendLine("                switch (remoteField.AssignedFieldId)");
         sb.AppendLine("                {");
-        foreach (MemberModel member in model.SortedMembers)
+        for (int idx = 0; idx < model.SortedMembers.Length; idx++)
         {
-            sb.AppendLine($"                    case \"{EscapeString(member.FieldIdentifier)}\":");
+            MemberModel member = model.SortedMembers[idx];
+            sb.AppendLine($"                    case {idx}:");
             sb.AppendLine("                        {");
-            EmitReadMemberAssignment(sb, member, "remoteRefMode", "remoteReadTypeInfo", "value", "Compat", 7, false);
+            EmitReadMemberAssignment(
+                sb,
+                member,
+                "remoteRefMode",
+                BuildCompatibleReadTypeInfoExpression(member),
+                "value",
+                "Compat",
+                7,
+                false);
             sb.AppendLine("                            break;");
             sb.AppendLine("                        }");
         }
@@ -435,7 +629,7 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
         string memberAccess = $"value.{member.Name}";
         string hasGenerics = member.IsCollection ? "true" : "false";
         string writeTypeInfo = compatibleMode
-            ? $"context.TypeResolver.GetTypeInfo<{member.TypeName}>().NeedsTypeInfoForField()"
+            ? BuildCompatibleTypeInfoExpression(member)
             : "false";
 
         switch (member.DynamicAnyKind)
@@ -475,6 +669,31 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
             return;
         }
 
+        if (writeTypeInfo == "false")
+        {
+            if (CanUseDirectWriteDataInvocation(member))
+            {
+                sb.AppendLine(
+                    $"            context.TypeResolver.GetSerializer<{member.TypeName}>().WriteData(context, {memberAccess}, {hasGenerics});");
+                return;
+            }
+
+            if (CanUseTrackRefBranchWriteDataInvocation(member))
+            {
+                sb.AppendLine("            if (context.TrackRef)");
+                sb.AppendLine("            {");
+                sb.AppendLine(
+                    $"                context.TypeResolver.GetSerializer<{member.TypeName}>().Write(context, {memberAccess}, global::Apache.Fory.RefMode.Tracking, false, {hasGenerics});");
+                sb.AppendLine("            }");
+                sb.AppendLine("            else");
+                sb.AppendLine("            {");
+                sb.AppendLine(
+                    $"                context.TypeResolver.GetSerializer<{member.TypeName}>().WriteData(context, {memberAccess}, {hasGenerics});");
+                sb.AppendLine("            }");
+                return;
+            }
+        }
+
         sb.AppendLine(
             $"            context.TypeResolver.GetSerializer<{member.TypeName}>().Write(context, {memberAccess}, {refModeExpr}, {writeTypeInfo}, {hasGenerics});");
     }
@@ -488,11 +707,11 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
         string hasGenerics,
         bool compatibleMode)
     {
-        string cacheId = Sanitize(member.Name);
+        string memberId = Sanitize(member.Name);
         string modeSuffix = compatibleMode ? "Compat" : "Schema";
-        string fieldValueVar = $"__{cacheId}DictValue{modeSuffix}";
-        string runtimeTypeVar = $"__{cacheId}DictRuntimeType{modeSuffix}";
-        string typeInfoVar = $"__{cacheId}DictTypeInfo{modeSuffix}";
+        string fieldValueVar = $"__{memberId}DictValue{modeSuffix}";
+        string runtimeTypeVar = $"__{memberId}DictRuntimeType{modeSuffix}";
+        string typeInfoVar = $"__{memberId}DictTypeInfo{modeSuffix}";
         sb.AppendLine($"            {member.TypeName} {fieldValueVar} = {memberAccess};");
         sb.AppendLine($"            if ({fieldValueVar} is null)");
         sb.AppendLine("            {");
@@ -502,17 +721,7 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
         sb.AppendLine("            else");
         sb.AppendLine("            {");
         sb.AppendLine($"                global::System.Type {runtimeTypeVar} = {fieldValueVar}.GetType();");
-        sb.AppendLine($"                global::Apache.Fory.TypeInfo {typeInfoVar};");
-        sb.AppendLine($"                if (__Fory{cacheId}DictRuntimeType == {runtimeTypeVar} && __Fory{cacheId}DictTypeInfo is not null)");
-        sb.AppendLine("                {");
-        sb.AppendLine($"                    {typeInfoVar} = __Fory{cacheId}DictTypeInfo;");
-        sb.AppendLine("                }");
-        sb.AppendLine("                else");
-        sb.AppendLine("                {");
-        sb.AppendLine($"                    {typeInfoVar} = context.TypeResolver.GetTypeInfo({runtimeTypeVar});");
-        sb.AppendLine($"                    __Fory{cacheId}DictRuntimeType = {runtimeTypeVar};");
-        sb.AppendLine($"                    __Fory{cacheId}DictTypeInfo = {typeInfoVar};");
-        sb.AppendLine("                }");
+        sb.AppendLine($"                global::Apache.Fory.TypeInfo {typeInfoVar} = context.TypeResolver.GetTypeInfo({runtimeTypeVar});");
         sb.AppendLine(
             $"                context.TypeResolver.WriteObject({typeInfoVar}, context, {fieldValueVar}, {refModeExpr}, {writeTypeInfo}, {hasGenerics});");
         sb.AppendLine("            }");
@@ -790,6 +999,26 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
         return member.Classification.IsPrimitive || member.Classification.TypeId == 21;
     }
 
+    private static bool CanUseDirectWriteDataInvocation(MemberModel member)
+    {
+        if (member.IsNullable || member.DynamicAnyKind != DynamicAnyKind.None)
+        {
+            return false;
+        }
+
+        return member.Classification.IsBuiltIn || !member.IsReferenceTrackableType;
+    }
+
+    private static bool CanUseTrackRefBranchWriteDataInvocation(MemberModel member)
+    {
+        if (member.IsNullable || member.DynamicAnyKind != DynamicAnyKind.None)
+        {
+            return false;
+        }
+
+        return !member.Classification.IsBuiltIn && member.IsReferenceTrackableType;
+    }
+
     private static string BuildSchemaFingerprintExpression(ImmutableArray<MemberModel> members)
     {
         if (members.IsDefaultOrEmpty)
@@ -811,9 +1040,9 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
             string trackRefExpr = member.DynamicAnyKind switch
             {
                 DynamicAnyKind.AnyValue => "(trackRef ? 1 : 0)",
-                _ => member.Classification.IsBuiltIn
+                _ => member.Classification.IsBuiltIn || !member.IsReferenceTrackableType
                     ? "0"
-                    : $"((trackRef && typeResolver.GetTypeInfo<{member.TypeName}>().IsReferenceTrackableType) ? 1 : 0)",
+                    : "(trackRef ? 1 : 0)",
             };
             string nullable = member.IsNullable ? "1" : "0";
             string piece = $"\"{EscapeString(member.FieldIdentifier)},{fingerprintTypeId},\" + {trackRefExpr} + \",{nullable};\"";
@@ -844,17 +1073,32 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
         return $"new global::Apache.Fory.TypeMetaFieldType({model.TypeIdExpr}, {BoolLiteral(model.Nullable)}, {localTrackRefExpr})";
     }
 
+    private static string BuildTypeMetaFieldIdExpression(short? fieldId)
+    {
+        return fieldId.HasValue ? $"(short){fieldId.Value}" : "null";
+    }
+
     private static string BuildWriteRefModeExpression(MemberModel member)
     {
         return member.DynamicAnyKind switch
         {
             DynamicAnyKind.AnyValue => $"__ForyRefMode({BoolLiteral(member.IsNullable)}, context.TrackRef)",
-            _ => member.Classification.IsBuiltIn
+            _ => member.Classification.IsBuiltIn || !member.IsReferenceTrackableType
                 ? $"__ForyRefMode({BoolLiteral(member.IsNullable)}, false)"
-                : $"__ForyRefMode({BoolLiteral(member.IsNullable)}, context.TrackRef && context.TypeResolver.GetTypeInfo<{member.TypeName}>().IsReferenceTrackableType)",
+                : $"__ForyRefMode({BoolLiteral(member.IsNullable)}, context.TrackRef)",
         };
     }
 
+    private static string BuildCompatibleReadTypeInfoExpression(MemberModel member)
+    {
+        return $"context.TypeResolver.GetTypeInfo<{member.TypeName}>().NeedsTypeInfoForField()";
+    }
+
+    private static string BuildCompatibleTypeInfoExpression(MemberModel member)
+    {
+        return $"context.TypeResolver.GetTypeInfo<{member.TypeName}>().NeedsTypeInfoForField()";
+    }
+
     private static TypeModel? BuildTypeModel(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken)
     {
         _ = cancellationToken;
@@ -967,6 +1211,7 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
     {
         (bool isOptional, ITypeSymbol unwrappedType) = UnwrapNullable(memberType);
         FieldEncoding fieldEncoding = FieldEncoding.None;
+        short? fieldId = null;
         foreach (AttributeData attribute in memberSymbol.GetAttributes())
         {
             string? attrName = attribute.AttributeClass?.ToDisplayString();
@@ -977,6 +1222,16 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
 
             foreach (KeyValuePair<string, TypedConstant> namedArg in attribute.NamedArguments)
             {
+                if (string.Equals(namedArg.Key, "Id", StringComparison.Ordinal))
+                {
+                    if (TryGetNonNegativeShort(namedArg.Value, out short parsedFieldId))
+                    {
+                        fieldId = parsedFieldId;
+                    }
+
+                    continue;
+                }
+
                 if (!string.Equals(namedArg.Key, "Encoding", StringComparison.Ordinal))
                 {
                     continue;
@@ -1030,10 +1285,12 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
             isOptional,
             memberType is INamedTypeSymbol nts &&
             nts.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T,
+            fieldId,
             classification,
             group,
             classification.IsCollection || classification.IsMap,
             classification.IsMap && !IsTypeSealed(unwrappedType),
+            !unwrappedType.IsValueType && classification.TypeId != 21,
             dynamicAnyKind == DynamicAnyKind.None ? DynamicAnyKind.None : dynamicAnyKind,
             typeMeta);
     }
@@ -1049,6 +1306,21 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
         if (TryGetListElementType(unwrapped, out ITypeSymbol? listElementType))
         {
             bool elementNullable = GenericNullable(listElementType!);
+            if (!elementNullable &&
+                TryResolvePackedArrayTypeIdForElement(listElementType!) is uint packedArrayTypeId &&
+                explicitTypeId == packedArrayTypeId)
+            {
+                // Align compatible TypeMeta with C++ vector arithmetic handling:
+                // when the wire type is already classified as a packed array (e.g. int[]),
+                // use the specialized array TypeId directly instead of LIST<elem>.
+                // This keeps schema/type-meta bytes consistent across languages.
+                return new TypeMetaFieldTypeModel(
+                    packedArrayTypeId.ToString(),
+                    nullable,
+                    false,
+                    ImmutableArray<TypeMetaFieldTypeModel>.Empty);
+            }
+
             TypeMetaFieldTypeModel element = BuildTypeMetaFieldTypeModel(
                 listElementType!,
                 elementNullable,
@@ -1141,6 +1413,60 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
             ImmutableArray<TypeMetaFieldTypeModel>.Empty);
     }
 
+    private static bool TryGetNonNegativeShort(TypedConstant value, out short fieldId)
+    {
+        fieldId = default;
+        object? raw = value.Value;
+        if (raw is null)
+        {
+            return false;
+        }
+
+        long numeric;
+        switch (raw)
+        {
+            case byte v:
+                numeric = v;
+                break;
+            case sbyte v:
+                numeric = v;
+                break;
+            case short v:
+                numeric = v;
+                break;
+            case ushort v:
+                numeric = v;
+                break;
+            case int v:
+                numeric = v;
+                break;
+            case uint v:
+                numeric = v;
+                break;
+            case long v:
+                numeric = v;
+                break;
+            case ulong v:
+                if (v > (ulong)short.MaxValue)
+                {
+                    return false;
+                }
+
+                numeric = (long)v;
+                break;
+            default:
+                return false;
+        }
+
+        if (numeric < 0 || numeric > short.MaxValue)
+        {
+            return false;
+        }
+
+        fieldId = (short)numeric;
+        return true;
+    }
+
     private static ImmutableArray<MemberModel> SortMembers(ImmutableArray<MemberModel> members)
     {
         return members
@@ -1364,22 +1690,7 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
 
         if (type is IArrayTypeSymbol arrayType)
         {
-            TypeClassification elem = ClassifyType(arrayType.ElementType);
-            uint typeId = elem.TypeId switch
-            {
-                9 => 41,
-                1 => 43,
-                2 => 44,
-                3 => 45,
-                5 => 46,
-                7 => 47,
-                10 => 49,
-                12 => 50,
-                14 => 51,
-                19 => 55,
-                20 => 56,
-                _ => 22,
-            };
+            uint typeId = TryResolvePackedArrayTypeIdForElement(arrayType.ElementType) ?? 22;
             return new TypeClassification(typeId, false, true, true, false, false, 0);
         }
 
@@ -1540,6 +1851,32 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
         return false;
     }
 
+    private static uint? TryResolvePackedArrayTypeIdForElement(ITypeSymbol elementType)
+    {
+        (bool isNullable, ITypeSymbol unwrapped) = UnwrapNullable(elementType);
+        if (isNullable)
+        {
+            return null;
+        }
+
+        uint elementTypeId = ClassifyType(unwrapped).TypeId;
+        return elementTypeId switch
+        {
+            9 => 41,  // byte -> binary
+            1 => 43,  // bool -> bool array
+            2 => 44,  // sbyte -> int8 array
+            3 => 45,  // short -> int16 array
+            5 => 46,  // int -> int32 array
+            7 => 47,  // long -> int64 array
+            10 => 49, // ushort -> uint16 array
+            12 => 50, // uint -> uint32 array
+            14 => 51, // ulong -> uint64 array
+            19 => 55, // float -> float32 array
+            20 => 56, // double -> float64 array
+            _ => null,
+        };
+    }
+
     private static (bool, ITypeSymbol) UnwrapNullable(ITypeSymbol type)
     {
         if (type is INamedTypeSymbol named &&
@@ -1699,10 +2036,12 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
             string typeName,
             bool isNullable,
             bool isNullableValueType,
+            short? fieldId,
             TypeClassification classification,
             int group,
             bool isCollection,
             bool useDictionaryTypeInfoCache,
+            bool isReferenceTrackableType,
             DynamicAnyKind dynamicAnyKind,
             TypeMetaFieldTypeModel typeMeta)
         {
@@ -1713,10 +2052,12 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
             TypeName = typeName;
             IsNullable = isNullable;
             IsNullableValueType = isNullableValueType;
+            FieldId = fieldId;
             Classification = classification;
             Group = group;
             IsCollection = isCollection;
             UseDictionaryTypeInfoCache = useDictionaryTypeInfoCache;
+            IsReferenceTrackableType = isReferenceTrackableType;
             DynamicAnyKind = dynamicAnyKind;
             TypeMeta = typeMeta;
         }
@@ -1728,10 +2069,12 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
         public string TypeName { get; }
         public bool IsNullable { get; }
         public bool IsNullableValueType { get; }
+        public short? FieldId { get; }
         public TypeClassification Classification { get; }
         public int Group { get; }
         public bool IsCollection { get; }
         public bool UseDictionaryTypeInfoCache { get; }
+        public bool IsReferenceTrackableType { get; }
         public DynamicAnyKind DynamicAnyKind { get; }
         public TypeMetaFieldTypeModel TypeMeta { get; }
     }
diff --git a/csharp/src/Fory/Attributes.cs b/csharp/src/Fory/Attributes.cs
index 2d6749fd9..3e05e087f 100644
--- a/csharp/src/Fory/Attributes.cs
+++ b/csharp/src/Fory/Attributes.cs
@@ -50,6 +50,12 @@ public enum FieldEncoding
 [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
 public sealed class FieldAttribute : Attribute
 {
+    /// <summary>
+    /// Optional stable field tag id used for compatible metadata dispatch.
+    /// Use a non-negative value to emit numeric field ids instead of field names.
+    /// </summary>
+    public short Id { get; set; } = -1;
+
     /// <summary>
     /// Gets or sets the field encoding strategy used by generated serializers.
     /// </summary>
diff --git a/csharp/src/Fory/ByteBuffer.cs b/csharp/src/Fory/ByteBuffer.cs
index 34a28748d..eb3afc827 100644
--- a/csharp/src/Fory/ByteBuffer.cs
+++ b/csharp/src/Fory/ByteBuffer.cs
@@ -16,6 +16,7 @@
 // under the License.
 
 using System.Buffers.Binary;
+using System.Runtime.CompilerServices;
 
 namespace Apache.Fory;
 
@@ -34,11 +35,13 @@ public sealed class ByteWriter
 
     public IReadOnlyList<byte> Storage => new ArraySegment<byte>(_storage, 0, _count);
 
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
     public void Reserve(int additional)
     {
         EnsureCapacity(additional);
     }
 
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
     public void WriteUInt8(byte value)
     {
         EnsureCapacity(1);
@@ -46,84 +49,230 @@ public sealed class ByteWriter
         _count += 1;
     }
 
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
     public void WriteInt8(sbyte value)
     {
         WriteUInt8(unchecked((byte)value));
     }
 
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
     public void WriteUInt16(ushort value)
     {
         EnsureCapacity(2);
-        BinaryPrimitives.WriteUInt16LittleEndian(_storage.AsSpan(_count, 2), value);
-        _count += 2;
+        int index = _count;
+        if (BitConverter.IsLittleEndian)
+        {
+            Unsafe.WriteUnaligned(ref _storage[index], value);
+        }
+        else
+        {
+            BinaryPrimitives.WriteUInt16LittleEndian(_storage.AsSpan(index, 2), value);
+        }
+
+        _count = index + 2;
     }
 
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
     public void WriteInt16(short value)
     {
         WriteUInt16(unchecked((ushort)value));
     }
 
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
     public void WriteUInt32(uint value)
     {
         EnsureCapacity(4);
-        BinaryPrimitives.WriteUInt32LittleEndian(_storage.AsSpan(_count, 4), value);
-        _count += 4;
+        int index = _count;
+        if (BitConverter.IsLittleEndian)
+        {
+            Unsafe.WriteUnaligned(ref _storage[index], value);
+        }
+        else
+        {
+            BinaryPrimitives.WriteUInt32LittleEndian(_storage.AsSpan(index, 4), value);
+        }
+
+        _count = index + 4;
     }
 
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
     public void WriteInt32(int value)
     {
         WriteUInt32(unchecked((uint)value));
     }
 
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
     public void WriteUInt64(ulong value)
     {
         EnsureCapacity(8);
-        BinaryPrimitives.WriteUInt64LittleEndian(_storage.AsSpan(_count, 8), value);
-        _count += 8;
+        int index = _count;
+        if (BitConverter.IsLittleEndian)
+        {
+            Unsafe.WriteUnaligned(ref _storage[index], value);
+        }
+        else
+        {
+            BinaryPrimitives.WriteUInt64LittleEndian(_storage.AsSpan(index, 8), value);
+        }
+
+        _count = index + 8;
     }
 
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
     public void WriteInt64(long value)
     {
         WriteUInt64(unchecked((ulong)value));
     }
 
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
     public void WriteVarUInt32(uint value)
     {
         EnsureCapacity(5);
-        uint remaining = value;
-        while (remaining >= 0x80)
+        int index = _count;
+        if (value < (1u << 7))
         {
-            _storage[_count] = (byte)((remaining & 0x7F) | 0x80);
-            _count += 1;
-            remaining >>= 7;
+            _storage[index] = unchecked((byte)value);
+            _count = index + 1;
+            return;
         }
 
-        _storage[_count] = (byte)remaining;
-        _count += 1;
+        if (value < (1u << 14))
+        {
+            _storage[index] = unchecked((byte)((value & 0x7Fu) | 0x80u));
+            _storage[index + 1] = unchecked((byte)(value >> 7));
+            _count = index + 2;
+            return;
+        }
+
+        if (value < (1u << 21))
+        {
+            _storage[index] = unchecked((byte)((value & 0x7Fu) | 0x80u));
+            _storage[index + 1] = unchecked((byte)(((value >> 7) & 0x7Fu) | 0x80u));
+            _storage[index + 2] = unchecked((byte)(value >> 14));
+            _count = index + 3;
+            return;
+        }
+
+        if (value < (1u << 28))
+        {
+            _storage[index] = unchecked((byte)((value & 0x7Fu) | 0x80u));
+            _storage[index + 1] = unchecked((byte)(((value >> 7) & 0x7Fu) | 0x80u));
+            _storage[index + 2] = unchecked((byte)(((value >> 14) & 0x7Fu) | 0x80u));
+            _storage[index + 3] = unchecked((byte)(value >> 21));
+            _count = index + 4;
+            return;
+        }
+
+        _storage[index] = unchecked((byte)((value & 0x7Fu) | 0x80u));
+        _storage[index + 1] = unchecked((byte)(((value >> 7) & 0x7Fu) | 0x80u));
+        _storage[index + 2] = unchecked((byte)(((value >> 14) & 0x7Fu) | 0x80u));
+        _storage[index + 3] = unchecked((byte)(((value >> 21) & 0x7Fu) | 0x80u));
+        _storage[index + 4] = unchecked((byte)(value >> 28));
+        _count = index + 5;
     }
 
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
     public void WriteVarUInt64(ulong value)
     {
-        EnsureCapacity(10);
-        ulong remaining = value;
-        for (var i = 0; i < 8; i++)
+        EnsureCapacity(9);
+        int index = _count;
+        if (value < (1UL << 7))
         {
-            if (remaining < 0x80)
-            {
-                _storage[_count] = (byte)remaining;
-                _count += 1;
-                return;
-            }
+            _storage[index] = unchecked((byte)value);
+            _count = index + 1;
+            return;
+        }
 
-            _storage[_count] = (byte)((remaining & 0x7F) | 0x80);
-            _count += 1;
-            remaining >>= 7;
+        if (value < (1UL << 14))
+        {
+            _storage[index] = unchecked((byte)((value & 0x7FuL) | 0x80uL));
+            _storage[index + 1] = unchecked((byte)(value >> 7));
+            _count = index + 2;
+            return;
         }
 
-        _storage[_count] = (byte)(remaining & 0xFF);
-        _count += 1;
+        if (value < (1UL << 21))
+        {
+            _storage[index] = unchecked((byte)((value & 0x7FuL) | 0x80uL));
+            _storage[index + 1] = unchecked((byte)(((value >> 7) & 0x7FuL) | 0x80uL));
+            _storage[index + 2] = unchecked((byte)(value >> 14));
+            _count = index + 3;
+            return;
+        }
+
+        if (value < (1UL << 28))
+        {
+            _storage[index] = unchecked((byte)((value & 0x7FuL) | 0x80uL));
+            _storage[index + 1] = unchecked((byte)(((value >> 7) & 0x7FuL) | 0x80uL));
+            _storage[index + 2] = unchecked((byte)(((value >> 14) & 0x7FuL) | 0x80uL));
+            _storage[index + 3] = unchecked((byte)(value >> 21));
+            _count = index + 4;
+            return;
+        }
+
+        if (value < (1UL << 35))
+        {
+            _storage[index] = unchecked((byte)((value & 0x7FuL) | 0x80uL));
+            _storage[index + 1] = unchecked((byte)(((value >> 7) & 0x7FuL) | 0x80uL));
+            _storage[index + 2] = unchecked((byte)(((value >> 14) & 0x7FuL) | 0x80uL));
+            _storage[index + 3] = unchecked((byte)(((value >> 21) & 0x7FuL) | 0x80uL));
+            _storage[index + 4] = unchecked((byte)(value >> 28));
+            _count = index + 5;
+            return;
+        }
+
+        if (value < (1UL << 42))
+        {
+            _storage[index] = unchecked((byte)((value & 0x7FuL) | 0x80uL));
+            _storage[index + 1] = unchecked((byte)(((value >> 7) & 0x7FuL) | 0x80uL));
+            _storage[index + 2] = unchecked((byte)(((value >> 14) & 0x7FuL) | 0x80uL));
+            _storage[index + 3] = unchecked((byte)(((value >> 21) & 0x7FuL) | 0x80uL));
+            _storage[index + 4] = unchecked((byte)(((value >> 28) & 0x7FuL) | 0x80uL));
+            _storage[index + 5] = unchecked((byte)(value >> 35));
+            _count = index + 6;
+            return;
+        }
+
+        if (value < (1UL << 49))
+        {
+            _storage[index] = unchecked((byte)((value & 0x7FuL) | 0x80uL));
+            _storage[index + 1] = unchecked((byte)(((value >> 7) & 0x7FuL) | 0x80uL));
+            _storage[index + 2] = unchecked((byte)(((value >> 14) & 0x7FuL) | 0x80uL));
+            _storage[index + 3] = unchecked((byte)(((value >> 21) & 0x7FuL) | 0x80uL));
+            _storage[index + 4] = unchecked((byte)(((value >> 28) & 0x7FuL) | 0x80uL));
+            _storage[index + 5] = unchecked((byte)(((value >> 35) & 0x7FuL) | 0x80uL));
+            _storage[index + 6] = unchecked((byte)(value >> 42));
+            _count = index + 7;
+            return;
+        }
+
+        if (value < (1UL << 56))
+        {
+            _storage[index] = unchecked((byte)((value & 0x7FuL) | 0x80uL));
+            _storage[index + 1] = unchecked((byte)(((value >> 7) & 0x7FuL) | 0x80uL));
+            _storage[index + 2] = unchecked((byte)(((value >> 14) & 0x7FuL) | 0x80uL));
+            _storage[index + 3] = unchecked((byte)(((value >> 21) & 0x7FuL) | 0x80uL));
+            _storage[index + 4] = unchecked((byte)(((value >> 28) & 0x7FuL) | 0x80uL));
+            _storage[index + 5] = unchecked((byte)(((value >> 35) & 0x7FuL) | 0x80uL));
+            _storage[index + 6] = unchecked((byte)(((value >> 42) & 0x7FuL) | 0x80uL));
+            _storage[index + 7] = unchecked((byte)(value >> 49));
+            _count = index + 8;
+            return;
+        }
+
+        _storage[index] = unchecked((byte)((value & 0x7FuL) | 0x80uL));
+        _storage[index + 1] = unchecked((byte)(((value >> 7) & 0x7FuL) | 0x80uL));
+        _storage[index + 2] = unchecked((byte)(((value >> 14) & 0x7FuL) | 0x80uL));
+        _storage[index + 3] = unchecked((byte)(((value >> 21) & 0x7FuL) | 0x80uL));
+        _storage[index + 4] = unchecked((byte)(((value >> 28) & 0x7FuL) | 0x80uL));
+        _storage[index + 5] = unchecked((byte)(((value >> 35) & 0x7FuL) | 0x80uL));
+        _storage[index + 6] = unchecked((byte)(((value >> 42) & 0x7FuL) | 0x80uL));
+        _storage[index + 7] = unchecked((byte)(((value >> 49) & 0x7FuL) | 0x80uL));
+        _storage[index + 8] = unchecked((byte)(value >> 56));
+        _count = index + 9;
     }
 
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
     public void WriteVarUInt36Small(ulong value)
     {
         if (value >= (1UL << 36))
@@ -134,18 +283,21 @@ public sealed class ByteWriter
         WriteVarUInt64(value);
     }
 
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
     public void WriteVarInt32(int value)
     {
         uint zigzag = unchecked((uint)((value << 1) ^ (value >> 31)));
         WriteVarUInt32(zigzag);
     }
 
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
     public void WriteVarInt64(long value)
     {
         ulong zigzag = unchecked((ulong)((value << 1) ^ (value >> 63)));
         WriteVarUInt64(zigzag);
     }
 
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
     public void WriteTaggedInt64(long value)
     {
         if (value >= -1_073_741_824L && value <= 1_073_741_823L)
@@ -158,6 +310,7 @@ public sealed class ByteWriter
         WriteInt64(value);
     }
 
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
     public void WriteTaggedUInt64(ulong value)
     {
         if (value <= int.MaxValue)
@@ -170,16 +323,19 @@ public sealed class ByteWriter
         WriteUInt64(value);
     }
 
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
     public void WriteFloat32(float value)
     {
         WriteUInt32(unchecked((uint)BitConverter.SingleToInt32Bits(value)));
     }
 
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
     public void WriteFloat64(double value)
     {
         WriteUInt64(unchecked((ulong)BitConverter.DoubleToInt64Bits(value)));
     }
 
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
     public void WriteBytes(ReadOnlySpan<byte> bytes)
     {
         EnsureCapacity(bytes.Length);
@@ -187,6 +343,7 @@ public sealed class ByteWriter
         _count += bytes.Length;
     }
 
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
     public Span<byte> GetSpan(int size)
     {
         if (size < 0)
@@ -198,6 +355,7 @@ public sealed class ByteWriter
         return _storage.AsSpan(_count, size);
     }
 
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
     public void Advance(int count)
     {
         if (count < 0 || _count + count > _storage.Length)
@@ -208,6 +366,7 @@ public sealed class ByteWriter
         _count += count;
     }
 
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
     public void SetByte(int index, byte value)
     {
         if ((uint)index >= (uint)_count)
@@ -218,6 +377,7 @@ public sealed class ByteWriter
         _storage[index] = value;
     }
 
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
     public void SetBytes(int index, ReadOnlySpan<byte> bytes)
     {
         if (index < 0 || index + bytes.Length > _count)
@@ -240,14 +400,26 @@ public sealed class ByteWriter
         _count = 0;
     }
 
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
     private void EnsureCapacity(int additional)
     {
+        if (additional <= 0)
+        {
+            return;
+        }
+
         int required = _count + additional;
         if (required <= _storage.Length)
         {
             return;
         }
 
+        Grow(required);
+    }
+
+    [MethodImpl(MethodImplOptions.NoInlining)]
+    private void Grow(int required)
+    {
         int next = _storage.Length * 2;
         if (next < required)
         {
@@ -373,8 +545,21 @@ public sealed class ByteReader
         byte[] storage = _storage;
         int cursor = _cursor;
         int length = _length;
-        uint result = 0;
-        int shift = 0;
+        if (cursor >= length)
+        {
+            throw new OutOfBoundsException(cursor, 1, length);
+        }
+
+        byte first = storage[cursor];
+        if ((first & 0x80) == 0)
+        {
+            _cursor = cursor + 1;
+            return first;
+        }
+
+        cursor += 1;
+        uint result = (uint)(first & 0x7F);
+        int shift = 7;
         while (true)
         {
             if (cursor >= length)
@@ -404,9 +589,22 @@ public sealed class ByteReader
         byte[] storage = _storage;
         int cursor = _cursor;
         int length = _length;
-        ulong result = 0;
-        int shift = 0;
-        for (var i = 0; i < 8; i++)
+        if (cursor >= length)
+        {
+            throw new OutOfBoundsException(cursor, 1, length);
+        }
+
+        byte first = storage[cursor];
+        if ((first & 0x80) == 0)
+        {
+            _cursor = cursor + 1;
+            return first;
+        }
+
+        cursor += 1;
+        ulong result = (ulong)(first & 0x7F);
+        int shift = 7;
+        for (var i = 1; i < 8; i++)
         {
             if (cursor >= length)
             {
diff --git a/csharp/src/Fory/CollectionSerializers.cs b/csharp/src/Fory/CollectionSerializers.cs
index db05e99a6..296f4ce3a 100644
--- a/csharp/src/Fory/CollectionSerializers.cs
+++ b/csharp/src/Fory/CollectionSerializers.cs
@@ -46,6 +46,36 @@ internal static class CollectionCodec
         return typeof(T).IsSealed;
     }
 
+    private static bool CanDeclareRuntimeElementType<T>(List<T> list, TypeInfo typeInfo)
+    {
+        if (list.Count == 0 ||
+            typeInfo.IsDynamicType ||
+            typeInfo.IsBuiltinType ||
+            !typeInfo.NeedsTypeInfoForField() ||
+            typeof(T).IsSealed)
+        {
+            return false;
+        }
+
+        Type declaredType = typeof(T);
+        bool nullable = typeInfo.IsNullableType;
+        for (int i = 0; i < list.Count; i++)
+        {
+            T value = list[i];
+            if (nullable && value is null)
+            {
+                continue;
+            }
+
+            if (value is null || value.GetType() != declaredType)
+            {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
     public static void WriteCollectionData<T>(
         IEnumerable<T> values,
         Serializer<T> elementSerializer,
@@ -54,8 +84,9 @@ internal static class CollectionCodec
     {
         TypeInfo elementTypeInfo = context.TypeResolver.GetTypeInfo<T>();
         List<T> list = values as List<T> ?? [.. values];
-        context.Writer.WriteVarUInt32((uint)list.Count);
-        if (list.Count == 0)
+        int count = list.Count;
+        context.Writer.WriteVarUInt32((uint)count);
+        if (count == 0)
         {
             return;
         }
@@ -63,9 +94,9 @@ internal static class CollectionCodec
         bool hasNull = false;
         if (elementTypeInfo.IsNullableType)
         {
-            for (int i = 0; i < list.Count; i++)
+            for (int i = 0; i < count; i++)
             {
-                if (!context.TypeResolver.IsNoneObject(elementTypeInfo, list[i]))
+                if (list[i] is not null)
                 {
                     continue;
                 }
@@ -76,13 +107,16 @@ internal static class CollectionCodec
         }
 
         bool trackRef = context.TrackRef && elementTypeInfo.IsReferenceTrackableType;
-        bool declaredElementType = hasGenerics && CanDeclareElementType<T>(elementTypeInfo);
+        bool declaredElementType = hasGenerics &&
+                                   (CanDeclareElementType<T>(elementTypeInfo) ||
+                                    CanDeclareRuntimeElementType(list, elementTypeInfo));
         bool dynamicElementType = elementTypeInfo.IsDynamicType;
         bool writeDeclaredCompatibleTypeInfo =
             context.Compatible &&
             declaredElementType &&
             !dynamicElementType &&
-            elementTypeInfo.NeedsTypeInfoForField();
+            elementTypeInfo.NeedsTypeInfoForField() &&
+            !elementTypeInfo.SupportsCompatibleReadWithoutTypeMeta;
 
         byte header = dynamicElementType ? (byte)0 : CollectionBits.SameType;
         if (trackRef)
@@ -109,9 +143,9 @@ internal static class CollectionCodec
         if (dynamicElementType)
         {
             RefMode refMode = trackRef ? RefMode.Tracking : hasNull ? RefMode.NullOnly : RefMode.None;
-            foreach (T element in list)
+            for (int i = 0; i < count; i++)
             {
-                elementSerializer.Write(context, element, refMode, true, hasGenerics);
+                elementSerializer.Write(context, list[i], refMode, true, hasGenerics);
             }
 
             return;
@@ -119,9 +153,9 @@ internal static class CollectionCodec
 
         if (trackRef)
         {
-            foreach (T element in list)
+            for (int i = 0; i < count; i++)
             {
-                elementSerializer.Write(context, element, RefMode.Tracking, false, hasGenerics);
+                elementSerializer.Write(context, list[i], RefMode.Tracking, false, hasGenerics);
             }
 
             return;
@@ -129,9 +163,10 @@ internal static class CollectionCodec
 
         if (hasNull)
         {
-            foreach (T element in list)
+            for (int i = 0; i < count; i++)
             {
-                if (context.TypeResolver.IsNoneObject(elementTypeInfo, element))
+                T element = list[i];
+                if (element is null)
                 {
                     context.Writer.WriteInt8((sbyte)RefFlag.Null);
                 }
@@ -145,9 +180,9 @@ internal static class CollectionCodec
             return;
         }
 
-        foreach (T element in list)
+        for (int i = 0; i < count; i++)
         {
-            elementSerializer.WriteData(context, element, hasGenerics);
+            elementSerializer.WriteData(context, list[i], hasGenerics);
         }
     }
 
@@ -170,7 +205,8 @@ internal static class CollectionCodec
             context.Compatible &&
             declared &&
             !elementTypeInfo.IsDynamicType &&
-            elementTypeInfo.NeedsTypeInfoForField();
+            elementTypeInfo.NeedsTypeInfoForField() &&
+            !elementTypeInfo.SupportsCompatibleReadWithoutTypeMeta;
 
         List<T> values = new(length);
         if (!sameType)
@@ -235,6 +271,14 @@ internal static class CollectionCodec
             return values;
         }
 
+        ICompatibleNoTypeMetaReader<T>? compatibleNoTypeMetaReader =
+            context.Compatible &&
+            !readDeclaredCompatibleTypeInfo &&
+            elementTypeInfo.SupportsCompatibleReadWithoutTypeMeta &&
+            elementSerializer is ICompatibleNoTypeMetaReader<T> noTypeMetaReader
+                ? noTypeMetaReader
+                : null;
+
         if (hasNull)
         {
             for (int i = 0; i < length; i++)
@@ -246,7 +290,12 @@ internal static class CollectionCodec
                 }
                 else
                 {
-                    values.Add(ReadCollectionElementDataWithCanonicalization(elementSerializer, context, canonicalizeElements));
+                    values.Add(
+                        ReadCollectionElementDataWithCanonicalization(
+                            elementSerializer,
+                            compatibleNoTypeMetaReader,
+                            context,
+                            canonicalizeElements));
                 }
             }
         }
@@ -254,7 +303,12 @@ internal static class CollectionCodec
         {
             for (int i = 0; i < length; i++)
             {
-                values.Add(ReadCollectionElementDataWithCanonicalization(elementSerializer, context, canonicalizeElements));
+                values.Add(
+                    ReadCollectionElementDataWithCanonicalization(
+                        elementSerializer,
+                        compatibleNoTypeMetaReader,
+                        context,
+                        canonicalizeElements));
             }
         }
 
@@ -285,16 +339,21 @@ internal static class CollectionCodec
 
     private static T ReadCollectionElementDataWithCanonicalization<T>(
         Serializer<T> elementSerializer,
+        ICompatibleNoTypeMetaReader<T>? compatibleNoTypeMetaReader,
         ReadContext context,
         bool canonicalize)
     {
         if (!canonicalize)
         {
-            return elementSerializer.ReadData(context);
+            return compatibleNoTypeMetaReader is null
+                ? elementSerializer.ReadData(context)
+                : compatibleNoTypeMetaReader.ReadDataCompatibleNoTypeMeta(context);
         }
 
         int start = context.Reader.Cursor;
-        T value = elementSerializer.ReadData(context);
+        T value = compatibleNoTypeMetaReader is null
+            ? elementSerializer.ReadData(context)
+            : compatibleNoTypeMetaReader.ReadDataCompatibleNoTypeMeta(context);
         int end = context.Reader.Cursor;
         return context.CanonicalizeNonTrackingReference(value, start, end);
     }
diff --git a/csharp/src/Fory/Context.cs b/csharp/src/Fory/Context.cs
index 990d89674..c1771ee92 100644
--- a/csharp/src/Fory/Context.cs
+++ b/csharp/src/Fory/Context.cs
@@ -19,17 +19,49 @@ namespace Apache.Fory;
 
 public sealed class CompatibleTypeDefWriteState
 {
-    private readonly Dictionary<Type, uint> _typeIndexByType = [];
+    private Dictionary<Type, uint>? _typeIndexByType;
+    private Type? _firstType;
     private uint _nextIndex;
 
     public uint? LookupIndex(Type type)
     {
-        return _typeIndexByType.TryGetValue(type, out uint idx) ? idx : null;
+        if (_nextIndex == 0)
+        {
+            return null;
+        }
+
+        if (ReferenceEquals(_firstType, type))
+        {
+            return 0;
+        }
+
+        if (_typeIndexByType is not null && _typeIndexByType.TryGetValue(type, out uint idx))
+        {
+            return idx;
+        }
+
+        return null;
     }
 
     public (uint Index, bool IsNew) AssignIndexIfAbsent(Type type)
     {
-        if (_typeIndexByType.TryGetValue(type, out uint existing))
+        if (_nextIndex == 0)
+        {
+            _firstType = type;
+            _nextIndex = 1;
+            return (0, true);
+        }
+
+        if (ReferenceEquals(_firstType, type))
+        {
+            return (0, false);
+        }
+
+        if (_typeIndexByType is null)
+        {
+            _typeIndexByType = new Dictionary<Type, uint>();
+        }
+        else if (_typeIndexByType.TryGetValue(type, out uint existing))
         {
             return (existing, false);
         }
@@ -42,18 +74,39 @@ public sealed class CompatibleTypeDefWriteState
 
     public void Reset()
     {
-        _typeIndexByType.Clear();
+        _firstType = null;
+        _typeIndexByType?.Clear();
         _nextIndex = 0;
     }
 }
 
 public sealed class CompatibleTypeDefReadState
 {
+    private const int MaxParsedTypeDefEntries = 8192;
     private readonly List<TypeMeta> _typeMetas = [];
+    private readonly Dictionary<ulong, CachedTypeMetaEntry> _cachedTypeMetasByHeader = [];
+    private TypeMeta? _firstTypeMeta;
+    private bool _hasFirstTypeMeta;
+    private ulong _lastMetaHeader;
+    private CachedTypeMetaEntry _lastTypeMeta;
+    private bool _hasLastMetaHeader;
+
+    private readonly record struct CachedTypeMetaEntry(TypeMeta TypeMeta, int SkipBytesAfterHeader);
 
     public TypeMeta? TypeMetaAt(int index)
     {
-        return index >= 0 && index < _typeMetas.Count ? _typeMetas[index] : null;
+        if (index < 0)
+        {
+            return null;
+        }
+
+        if (index == 0)
+        {
+            return _hasFirstTypeMeta ? _firstTypeMeta : null;
+        }
+
+        int listIndex = index - 1;
+        return listIndex >= 0 && listIndex < _typeMetas.Count ? _typeMetas[listIndex] : null;
     }
 
     public void StoreTypeMeta(TypeMeta typeMeta, int index)
@@ -63,24 +116,76 @@ public sealed class CompatibleTypeDefReadState
             throw new InvalidDataException("negative compatible type definition index");
         }
 
-        if (index == _typeMetas.Count)
+        if (index == 0)
+        {
+            _firstTypeMeta = typeMeta;
+            _hasFirstTypeMeta = true;
+            return;
+        }
+
+        if (!_hasFirstTypeMeta)
+        {
+            throw new InvalidDataException(
+                $"compatible type definition index gap: index={index}, missing index 0");
+        }
+
+        int listIndex = index - 1;
+        if (listIndex == _typeMetas.Count)
         {
             _typeMetas.Add(typeMeta);
             return;
         }
 
-        if (index < _typeMetas.Count)
+        if (listIndex < _typeMetas.Count)
         {
-            _typeMetas[index] = typeMeta;
+            _typeMetas[listIndex] = typeMeta;
             return;
         }
 
         throw new InvalidDataException(
-            $"compatible type definition index gap: index={index}, count={_typeMetas.Count}");
+            $"compatible type definition index gap: index={index}, count={_typeMetas.Count + 1}");
+    }
+
+    public bool TryGetCachedTypeMeta(ulong header, out TypeMeta typeMeta, out int skipBytesAfterHeader)
+    {
+        if (_hasLastMetaHeader && _lastMetaHeader == header)
+        {
+            typeMeta = _lastTypeMeta.TypeMeta;
+            skipBytesAfterHeader = _lastTypeMeta.SkipBytesAfterHeader;
+            return true;
+        }
+
+        if (_cachedTypeMetasByHeader.TryGetValue(header, out CachedTypeMetaEntry cached))
+        {
+            _lastMetaHeader = header;
+            _lastTypeMeta = cached;
+            _hasLastMetaHeader = true;
+            typeMeta = cached.TypeMeta;
+            skipBytesAfterHeader = cached.SkipBytesAfterHeader;
+            return true;
+        }
+
+        typeMeta = null!;
+        skipBytesAfterHeader = 0;
+        return false;
+    }
+
+    public void CacheTypeMeta(ulong header, TypeMeta typeMeta, int skipBytesAfterHeader)
+    {
+        CachedTypeMetaEntry cached = new(typeMeta, skipBytesAfterHeader);
+        _lastMetaHeader = header;
+        _lastTypeMeta = cached;
+        _hasLastMetaHeader = true;
+        if (_cachedTypeMetasByHeader.Count < MaxParsedTypeDefEntries)
+        {
+            _cachedTypeMetasByHeader.TryAdd(header, cached);
+        }
     }
 
     public void Reset()
     {
+        _firstTypeMeta = null;
+        _hasFirstTypeMeta = false;
         _typeMetas.Clear();
     }
 }
@@ -186,12 +291,17 @@ public sealed class WriteContext
     }
 
     public void WriteCompatibleTypeMeta(Type type, TypeMeta typeMeta)
+    {
+        WriteCompatibleTypeMeta(type, typeMeta.Encode());
+    }
+
+    public void WriteCompatibleTypeMeta(Type type, ReadOnlySpan<byte> encodedTypeMeta)
     {
         (uint index, bool isNew) = CompatibleTypeDefState.AssignIndexIfAbsent(type);
         if (isNew)
         {
             Writer.WriteVarUInt32(index << 1);
-            Writer.WriteBytes(typeMeta.Encode());
+            Writer.WriteBytes(encodedTypeMeta);
         }
         else
         {
@@ -226,10 +336,14 @@ internal sealed class CanonicalReferenceEntry
 
 public sealed class ReadContext
 {
-    private readonly Dictionary<Type, List<TypeMeta>> _pendingCompatibleTypeMeta = [];
     private readonly Dictionary<Type, DynamicTypeInfo> _pendingDynamicTypeInfo = [];
     private readonly Dictionary<CanonicalReferenceSignature, List<CanonicalReferenceEntry>> _canonicalReferenceCache = [];
     private readonly int _maxDynamicReadDepth;
+    private Type? _pendingCompatibleType;
+    private TypeMeta? _pendingCompatibleTypeMeta;
+    private Dictionary<Type, TypeMeta>? _pendingCompatibleTypeMetaMap;
+    private Type? _lastCompatibleType;
+    private TypeMeta? _lastCompatibleTypeMeta;
     private int _currentDynamicReadDepth;
 
     public ReadContext(
@@ -296,24 +410,118 @@ public sealed class ReadContext
             return cached;
         }
 
+        ulong header = Reader.ReadUInt64();
+        if (CompatibleTypeDefState.TryGetCachedTypeMeta(
+                header,
+                out TypeMeta cachedTypeMeta,
+                out int skipBytesAfterHeader))
+        {
+            Reader.Skip(skipBytesAfterHeader);
+            CompatibleTypeDefState.StoreTypeMeta(cachedTypeMeta, index);
+            return cachedTypeMeta;
+        }
+
+        int headerStartCursor = Reader.Cursor - sizeof(ulong);
+        Reader.MoveBack(sizeof(ulong));
         TypeMeta typeMeta = TypeMeta.Decode(Reader);
+        int consumedTypeMetaBytes = Reader.Cursor - headerStartCursor;
+        int parsedSkipBytesAfterHeader = consumedTypeMetaBytes - sizeof(ulong);
         CompatibleTypeDefState.StoreTypeMeta(typeMeta, index);
+        CompatibleTypeDefState.CacheTypeMeta(header, typeMeta, parsedSkipBytesAfterHeader);
         return typeMeta;
     }
 
     public void PushCompatibleTypeMeta(Type type, TypeMeta typeMeta)
     {
-        _pendingCompatibleTypeMeta[type] = [typeMeta];
+        if (_lastCompatibleType == type && ReferenceEquals(_lastCompatibleTypeMeta, typeMeta))
+        {
+            return;
+        }
+
+        if (ReferenceEquals(_pendingCompatibleType, type))
+        {
+            if (ReferenceEquals(_pendingCompatibleTypeMeta, typeMeta))
+            {
+                _lastCompatibleType = type;
+                _lastCompatibleTypeMeta = typeMeta;
+                return;
+            }
+
+            _pendingCompatibleTypeMeta = typeMeta;
+            _lastCompatibleType = type;
+            _lastCompatibleTypeMeta = typeMeta;
+            return;
+        }
+
+        if (_pendingCompatibleType is null)
+        {
+            _pendingCompatibleType = type;
+            _pendingCompatibleTypeMeta = typeMeta;
+            _lastCompatibleType = type;
+            _lastCompatibleTypeMeta = typeMeta;
+            return;
+        }
+
+        if (_pendingCompatibleTypeMetaMap is null)
+        {
+            _pendingCompatibleTypeMetaMap = new Dictionary<Type, TypeMeta>();
+            if (_pendingCompatibleTypeMeta is not null)
+            {
+                _pendingCompatibleTypeMetaMap[_pendingCompatibleType] = _pendingCompatibleTypeMeta;
+            }
+        }
+        else if (_pendingCompatibleTypeMetaMap.TryGetValue(type, out TypeMeta? existing) &&
+                 ReferenceEquals(existing, typeMeta))
+        {
+            _lastCompatibleType = type;
+            _lastCompatibleTypeMeta = typeMeta;
+            return;
+        }
+
+        _pendingCompatibleTypeMetaMap[type] = typeMeta;
+        _lastCompatibleType = type;
+        _lastCompatibleTypeMeta = typeMeta;
+    }
+
+    public bool TryConsumeCompatibleTypeMeta(Type type, out TypeMeta? typeMeta)
+    {
+        if (_lastCompatibleType == type && _lastCompatibleTypeMeta is not null)
+        {
+            typeMeta = _lastCompatibleTypeMeta;
+            return true;
+        }
+
+        if (ReferenceEquals(_pendingCompatibleType, type) &&
+            _pendingCompatibleTypeMeta is not null)
+        {
+            _lastCompatibleType = type;
+            _lastCompatibleTypeMeta = _pendingCompatibleTypeMeta;
+            typeMeta = _pendingCompatibleTypeMeta;
+            return true;
+        }
+
+        if (_pendingCompatibleTypeMetaMap is null ||
+            !_pendingCompatibleTypeMetaMap.TryGetValue(type, out TypeMeta? pendingTypeMeta) ||
+            pendingTypeMeta is null)
+        {
+            typeMeta = null;
+            return false;
+        }
+
+        _lastCompatibleType = type;
+        _lastCompatibleTypeMeta = pendingTypeMeta;
+        typeMeta = pendingTypeMeta;
+        return true;
     }
 
     public TypeMeta ConsumeCompatibleTypeMeta(Type type)
     {
-        if (!_pendingCompatibleTypeMeta.TryGetValue(type, out List<TypeMeta>? stack) || stack.Count == 0)
+        if (TryConsumeCompatibleTypeMeta(type, out TypeMeta? typeMeta) && typeMeta is not null)
         {
-            throw new InvalidDataException($"missing compatible type metadata for {type}");
+            return typeMeta;
         }
 
-        return stack[^1];
+        throw new InvalidDataException($"missing compatible type metadata for {type}");
     }
 
     public void SetDynamicTypeInfo(Type type, DynamicTypeInfo typeInfo)
@@ -385,9 +593,13 @@ public sealed class ReadContext
     public void ResetObjectState()
     {
         RefReader.Reset();
-        _pendingCompatibleTypeMeta.Clear();
+        _pendingCompatibleType = null;
+        _pendingCompatibleTypeMeta = null;
+        _pendingCompatibleTypeMetaMap?.Clear();
         _pendingDynamicTypeInfo.Clear();
         _canonicalReferenceCache.Clear();
+        _lastCompatibleType = null;
+        _lastCompatibleTypeMeta = null;
         _currentDynamicReadDepth = 0;
     }
 
diff --git a/csharp/src/Fory/DictionarySerializers.cs b/csharp/src/Fory/DictionarySerializers.cs
index 8f7605853..5a4761882 100644
--- a/csharp/src/Fory/DictionarySerializers.cs
+++ b/csharp/src/Fory/DictionarySerializers.cs
@@ -70,12 +70,14 @@ public abstract class DictionaryLikeSerializer<TDictionary, TKey, TValue> : Seri
             context.Compatible &&
             keyDeclared &&
             !keyDynamicType &&
-            keyTypeInfo.NeedsTypeInfoForField();
+            keyTypeInfo.NeedsTypeInfoForField() &&
+            !keyTypeInfo.SupportsCompatibleReadWithoutTypeMeta;
         bool writeDeclaredCompatibleValueTypeInfo =
             context.Compatible &&
             valueDeclared &&
             !valueDynamicType &&
-            valueTypeInfo.NeedsTypeInfoForField();
+            valueTypeInfo.NeedsTypeInfoForField() &&
+            !valueTypeInfo.SupportsCompatibleReadWithoutTypeMeta;
 
         KeyValuePair<TKey, TValue>[] pairs = SnapshotPairs(map);
         if (keyDynamicType || valueDynamicType)
@@ -248,12 +250,14 @@ public abstract class DictionaryLikeSerializer<TDictionary, TKey, TValue> : Seri
                 context.Compatible &&
                 keyDeclared &&
                 !keyDynamicType &&
-                keyTypeInfo.NeedsTypeInfoForField();
+                keyTypeInfo.NeedsTypeInfoForField() &&
+                !keyTypeInfo.SupportsCompatibleReadWithoutTypeMeta;
             bool readDeclaredCompatibleValueTypeInfo =
                 context.Compatible &&
                 valueDeclared &&
                 !valueDynamicType &&
-                valueTypeInfo.NeedsTypeInfoForField();
+                valueTypeInfo.NeedsTypeInfoForField() &&
+                !valueTypeInfo.SupportsCompatibleReadWithoutTypeMeta;
 
             if (keyNull && valueNull)
             {
diff --git a/csharp/src/Fory/Fory.cs b/csharp/src/Fory/Fory.cs
index d428ec2bc..00bb275f8 100644
--- a/csharp/src/Fory/Fory.cs
+++ b/csharp/src/Fory/Fory.cs
@@ -146,8 +146,7 @@ public sealed class Fory
         ByteWriter writer = _writeContext.Writer;
         writer.Reset();
         Serializer<T> serializer = _typeResolver.GetSerializer<T>();
-        TypeInfo typeInfo = _typeResolver.GetTypeInfo<T>();
-        bool isNone = typeInfo.IsNullableType && value is null;
+        bool isNone = value is null;
         WriteHead(writer, isNone);
         if (!isNone)
         {
diff --git a/csharp/src/Fory/NullableKeyDictionary.cs b/csharp/src/Fory/NullableKeyDictionary.cs
index 1f9de8843..c083bff86 100644
--- a/csharp/src/Fory/NullableKeyDictionary.cs
+++ b/csharp/src/Fory/NullableKeyDictionary.cs
@@ -415,12 +415,14 @@ public sealed class NullableKeyDictionarySerializer<TKey, TValue> : Serializer<N
             context.Compatible &&
             keyDeclared &&
             !keyDynamicType &&
-            keyTypeInfo.NeedsTypeInfoForField();
+            keyTypeInfo.NeedsTypeInfoForField() &&
+            !keyTypeInfo.SupportsCompatibleReadWithoutTypeMeta;
         bool writeDeclaredCompatibleValueTypeInfo =
             context.Compatible &&
             valueDeclared &&
             !valueDynamicType &&
-            valueTypeInfo.NeedsTypeInfoForField();
+            valueTypeInfo.NeedsTypeInfoForField() &&
+            !valueTypeInfo.SupportsCompatibleReadWithoutTypeMeta;
         KeyValuePair<TKey, TValue>[] pairs = [.. map];
         if (keyDynamicType || valueDynamicType)
         {
@@ -571,12 +573,14 @@ public sealed class NullableKeyDictionarySerializer<TKey, TValue> : Serializer<N
                 context.Compatible &&
                 keyDeclared &&
                 !keyDynamicType &&
-                keyTypeInfo.NeedsTypeInfoForField();
+                keyTypeInfo.NeedsTypeInfoForField() &&
+                !keyTypeInfo.SupportsCompatibleReadWithoutTypeMeta;
             bool readDeclaredCompatibleValueTypeInfo =
                 context.Compatible &&
                 valueDeclared &&
                 !valueDynamicType &&
-                valueTypeInfo.NeedsTypeInfoForField();
+                valueTypeInfo.NeedsTypeInfoForField() &&
+                !valueTypeInfo.SupportsCompatibleReadWithoutTypeMeta;
 
             if (keyNull && valueNull)
             {
diff --git a/csharp/src/Fory/Serializer.cs b/csharp/src/Fory/Serializer.cs
index 1aa219ea2..1ce947f8b 100644
--- a/csharp/src/Fory/Serializer.cs
+++ b/csharp/src/Fory/Serializer.cs
@@ -17,6 +17,11 @@
 
 namespace Apache.Fory;
 
+public interface ICompatibleNoTypeMetaReader<T>
+{
+    T ReadDataCompatibleNoTypeMeta(ReadContext context);
+}
+
 /// <summary>
 /// Base class for custom serializers.
 /// </summary>
@@ -121,6 +126,7 @@ public abstract class Serializer<T>
                         }
 
                         T value = ReadData(context);
+
                         context.RefReader.FinishPendingReferenceIfNeeded(value);
                         context.RefReader.PopPendingReference();
                         return value;
diff --git a/csharp/src/Fory/StringSerializer.cs b/csharp/src/Fory/StringSerializer.cs
index a27674422..78a822eb1 100644
--- a/csharp/src/Fory/StringSerializer.cs
+++ b/csharp/src/Fory/StringSerializer.cs
@@ -21,6 +21,7 @@ namespace Apache.Fory;
 
 public sealed class StringSerializer : Serializer<string>
 {
+    private const int MaxVarUInt36SmallBytes = 6;
 
 
     public override string DefaultValue => null!;
@@ -88,75 +89,84 @@ public sealed class StringSerializer : Serializer<string>
 
     private static ForyStringEncoding SelectEncoding(string value)
     {
-        int numChars = value.Length;
-        int sampleNum = Math.Min(64, numChars);
         int asciiCount = 0;
-        int latin1Count = 0;
-        for (int i = 0; i < sampleNum; i++)
+        bool allLatin1 = true;
+        for (int i = 0; i < value.Length; i++)
         {
             char c = value[i];
             if (c < 0x80)
             {
                 asciiCount++;
-                latin1Count++;
             }
-            else if (c <= 0xFF)
+            else if (c > 0xFF)
             {
-                latin1Count++;
+                allLatin1 = false;
             }
         }
 
-        if (latin1Count == numChars || (latin1Count == sampleNum && IsLatin(value, sampleNum)))
+        if (allLatin1)
         {
             return ForyStringEncoding.Latin1;
         }
 
-        return asciiCount * 2 >= sampleNum ? ForyStringEncoding.Utf8 : ForyStringEncoding.Utf16;
-    }
-
-    private static bool IsLatin(string value, int start)
-    {
-        for (int i = start; i < value.Length; i++)
-        {
-            if (value[i] > 0xFF)
-            {
-                return false;
-            }
-        }
-
-        return true;
+        return asciiCount * 2 >= value.Length ? ForyStringEncoding.Utf8 : ForyStringEncoding.Utf16;
     }
 
     private static void WriteLatin1(WriteContext context, string value)
     {
         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]);
-        }
-        context.Writer.Advance(byteLength);
+        Span<byte> headerBuf = stackalloc byte[MaxVarUInt36SmallBytes];
+        int headerBytes = EncodeVarUInt36Small(headerBuf, header);
+        Span<byte> destination = context.Writer.GetSpan(headerBytes + byteLength);
+        headerBuf.Slice(0, headerBytes).CopyTo(destination);
+        int written = Encoding.Latin1.GetBytes(value.AsSpan(), destination.Slice(headerBytes));
+        context.Writer.Advance(headerBytes + written);
     }
 
     private static void WriteUtf8(WriteContext context, string value)
     {
-        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);
+        int maxByteLength = Encoding.UTF8.GetMaxByteCount(value.Length);
+        Span<byte> destination = context.Writer.GetSpan(MaxVarUInt36SmallBytes + maxByteLength);
+        Span<byte> payload = destination.Slice(MaxVarUInt36SmallBytes);
+        int written = Encoding.UTF8.GetBytes(value.AsSpan(), payload);
+
+        ulong header = ((ulong)written << 2) | (ulong)ForyStringEncoding.Utf8;
+        Span<byte> headerBuf = stackalloc byte[MaxVarUInt36SmallBytes];
+        int headerBytes = EncodeVarUInt36Small(headerBuf, header);
+        if (headerBytes != MaxVarUInt36SmallBytes)
+        {
+            payload.Slice(0, written).CopyTo(destination.Slice(headerBytes));
+        }
+
+        headerBuf.Slice(0, headerBytes).CopyTo(destination);
+        context.Writer.Advance(headerBytes + written);
     }
 
     private static void WriteUtf16(WriteContext context, string value)
     {
-        int byteLength = Encoding.Unicode.GetByteCount(value);
+        int byteLength = checked(value.Length * 2);
         ulong header = ((ulong)byteLength << 2) | (ulong)ForyStringEncoding.Utf16;
-        context.Writer.WriteVarUInt36Small(header);
-        Span<byte> utf16 = context.Writer.GetSpan(byteLength);
-        int written = Encoding.Unicode.GetBytes(value, utf16);
-        context.Writer.Advance(written);
+        Span<byte> headerBuf = stackalloc byte[MaxVarUInt36SmallBytes];
+        int headerBytes = EncodeVarUInt36Small(headerBuf, header);
+        Span<byte> destination = context.Writer.GetSpan(headerBytes + byteLength);
+        headerBuf.Slice(0, headerBytes).CopyTo(destination);
+        int written = Encoding.Unicode.GetBytes(value.AsSpan(), destination.Slice(headerBytes));
+        context.Writer.Advance(headerBytes + written);
+    }
+
+    private static int EncodeVarUInt36Small(Span<byte> destination, ulong value)
+    {
+        int index = 0;
+        ulong remaining = value;
+        while (remaining >= 0x80)
+        {
+            destination[index] = unchecked((byte)((remaining & 0x7FuL) | 0x80uL));
+            index += 1;
+            remaining >>= 7;
+        }
+
+        destination[index] = unchecked((byte)remaining);
+        return index + 1;
     }
 }
diff --git a/csharp/src/Fory/TypeInfo.cs b/csharp/src/Fory/TypeInfo.cs
index f6262b22e..bdf445d39 100644
--- a/csharp/src/Fory/TypeInfo.cs
+++ b/csharp/src/Fory/TypeInfo.cs
@@ -31,6 +31,8 @@ internal enum UserTypeKind
 
 public sealed class TypeInfo
 {
+    internal readonly record struct CompatibleTypeMetaCacheEntry(TypeMeta TypeMeta, byte[] EncodedBytes, ulong HeaderHash);
+
     private readonly object _serializer;
     private readonly Action<WriteContext, object?, bool> _writeDataObject;
     private readonly Func<ReadContext, object?> _readDataObject;
@@ -47,7 +49,13 @@ public sealed class TypeInfo
         bool isDynamicType,
         bool isNullableType,
         bool isReferenceTrackableType,
+        bool supportsCompatibleReadWithoutTypeMeta,
         object? defaultObject,
+        bool isRegistered,
+        uint? userTypeId,
+        bool registerByName,
+        MetaString? namespaceName,
+        MetaString? typeName,
         Action<WriteContext, object?, bool> writeDataObject,
         Func<ReadContext, object?> readDataObject,
         Action<WriteContext, object?, RefMode, bool, bool> writeObject,
@@ -61,7 +69,13 @@ public sealed class TypeInfo
         IsDynamicType = isDynamicType;
         IsNullableType = isNullableType;
         IsReferenceTrackableType = isReferenceTrackableType;
+        SupportsCompatibleReadWithoutTypeMeta = supportsCompatibleReadWithoutTypeMeta;
         DefaultObject = defaultObject;
+        IsRegistered = isRegistered;
+        UserTypeId = userTypeId;
+        RegisterByName = registerByName;
+        NamespaceName = namespaceName;
+        TypeName = typeName;
         _writeDataObject = writeDataObject;
         _readDataObject = readDataObject;
         _writeObject = writeObject;
@@ -78,6 +92,7 @@ public sealed class TypeInfo
             hasCompatibleTypeMetaFieldsProvider);
         bool isNullableType = !type.IsValueType || Nullable.GetUnderlyingType(type) is not null;
         bool isReferenceTrackableType = type != typeof(string) && !type.IsValueType;
+        bool supportsCompatibleReadWithoutTypeMeta = ResolveSupportsCompatibleReadWithoutTypeMeta(serializer);
         return new TypeInfo(
             type,
             serializer,
@@ -86,7 +101,13 @@ public sealed class TypeInfo
             isDynamicType,
             isNullableType,
             isReferenceTrackableType,
+            supportsCompatibleReadWithoutTypeMeta,
             serializer.DefaultObject,
+            isRegistered: false,
+            userTypeId: null,
+            registerByName: false,
+            namespaceName: null,
+            typeName: null,
             (context, value, hasGenerics) => WriteDataObject(serializer, context, value, hasGenerics),
             context => serializer.ReadData(context),
             (context, value, refMode, writeTypeInfo, hasGenerics) =>
@@ -129,6 +150,29 @@ public sealed class TypeInfo
         return EmptyTypeMetaFields;
     }
 
+    private static bool ResolveSupportsCompatibleReadWithoutTypeMeta(object serializer)
+    {
+        MethodInfo? method = serializer.GetType().GetMethod(
+            "SupportsCompatibleReadWithoutTypeMeta",
+            BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
+            null,
+            Type.EmptyTypes,
+            null);
+        if (method is null || method.ReturnType != typeof(bool))
+        {
+            return false;
+        }
+
+        try
+        {
+            return (bool)method.Invoke(serializer, null)!;
+        }
+        catch
+        {
+            return false;
+        }
+    }
+
     private static void WriteDataObject<T>(Serializer<T> serializer, WriteContext context, object? value, bool hasGenerics)
     {
         serializer.WriteData(context, CoerceRuntimeValue(serializer, value), hasGenerics);
@@ -423,8 +467,12 @@ public sealed class TypeInfo
 
     public bool IsReferenceTrackableType { get; }
 
+    public bool SupportsCompatibleReadWithoutTypeMeta { get; }
+
     public object? DefaultObject { get; }
 
+    internal Type SerializerType => _serializer.GetType();
+
     public bool NeedsTypeInfoForField()
     {
         if (IsDynamicType)
@@ -475,40 +523,86 @@ public sealed class TypeInfo
         return _compatibleTypeMetaFields(trackRef);
     }
 
-    internal bool IsRegistered { get; private set; }
+    internal bool IsRegistered { get; }
 
-    internal uint? UserTypeId { get; private set; }
+    internal uint? UserTypeId { get; }
 
-    internal bool RegisterByName { get; private set; }
+    internal bool RegisterByName { get; }
 
-    internal MetaString? NamespaceName { get; private set; }
+    internal MetaString? NamespaceName { get; }
 
-    internal MetaString? TypeName { get; private set; }
+    internal MetaString? TypeName { get; }
 
-    internal void RegisterByTypeId(uint userTypeId)
+    internal TypeInfo WithTypeIdRegistration(uint userTypeId)
     {
-        IsRegistered = true;
-        UserTypeId = userTypeId;
-        RegisterByName = false;
-        NamespaceName = null;
-        TypeName = null;
+        return new TypeInfo(
+            Type,
+            _serializer,
+            BuiltInTypeId,
+            UserTypeKind,
+            IsDynamicType,
+            IsNullableType,
+            IsReferenceTrackableType,
+            SupportsCompatibleReadWithoutTypeMeta,
+            DefaultObject,
+            isRegistered: true,
+            userTypeId: userTypeId,
+            registerByName: false,
+            namespaceName: null,
+            typeName: null,
+            _writeDataObject,
+            _readDataObject,
+            _writeObject,
+            _readObject,
+            _compatibleTypeMetaFields);
     }
 
-    internal void RegisterByTypeName(MetaString namespaceName, MetaString typeName)
+    internal TypeInfo WithTypeNameRegistration(MetaString namespaceName, MetaString typeName)
     {
-        IsRegistered = true;
-        UserTypeId = null;
-        RegisterByName = true;
-        NamespaceName = namespaceName;
-        TypeName = typeName;
+        return new TypeInfo(
+            Type,
+            _serializer,
+            BuiltInTypeId,
+            UserTypeKind,
+            IsDynamicType,
+            IsNullableType,
+            IsReferenceTrackableType,
+            SupportsCompatibleReadWithoutTypeMeta,
+            DefaultObject,
+            isRegistered: true,
+            userTypeId: null,
+            registerByName: true,
+            namespaceName: namespaceName,
+            typeName: typeName,
+            _writeDataObject,
+            _readDataObject,
+            _writeObject,
+            _readObject,
+            _compatibleTypeMetaFields);
     }
 
-    internal void CopyRegistrationFrom(TypeInfo source)
+    internal TypeInfo WithRegistrationFrom(TypeInfo source)
     {
-        IsRegistered = source.IsRegistered;
-        UserTypeId = source.UserTypeId;
-        RegisterByName = source.RegisterByName;
-        NamespaceName = source.NamespaceName;
-        TypeName = source.TypeName;
+        if (!source.IsRegistered)
+        {
+            return this;
+        }
+
+        if (source.RegisterByName)
+        {
+            if (!source.NamespaceName.HasValue || !source.TypeName.HasValue)
+            {
+                throw new InvalidDataException("missing type name metadata for name-registered type");
+            }
+
+            return WithTypeNameRegistration(source.NamespaceName.Value, source.TypeName.Value);
+        }
+
+        if (!source.UserTypeId.HasValue)
+        {
+            throw new InvalidDataException("missing user type id metadata for id-registered type");
+        }
+
+        return WithTypeIdRegistration(source.UserTypeId.Value);
     }
 }
diff --git a/csharp/src/Fory/TypeMeta.cs b/csharp/src/Fory/TypeMeta.cs
index 977cef72d..ff05b507a 100644
--- a/csharp/src/Fory/TypeMeta.cs
+++ b/csharp/src/Fory/TypeMeta.cs
@@ -251,6 +251,7 @@ public sealed class TypeMetaFieldInfo : IEquatable<TypeMetaFieldInfo>
         FieldId = fieldId;
         FieldName = fieldName;
         FieldType = fieldType;
+        AssignedFieldId = -1;
     }
 
     public short? FieldId { get; }
@@ -259,6 +260,8 @@ public sealed class TypeMetaFieldInfo : IEquatable<TypeMetaFieldInfo>
 
     public TypeMetaFieldType FieldType { get; }
 
+    public int AssignedFieldId { get; internal set; }
+
     internal void Write(ByteWriter writer)
     {
         byte header = 0;
@@ -383,6 +386,8 @@ public sealed class TypeMetaFieldInfo : IEquatable<TypeMetaFieldInfo>
 
 public sealed class TypeMeta : IEquatable<TypeMeta>
 {
+    private bool _hasAssignedFieldIds;
+
     public TypeMeta(
         uint? typeId,
         uint? userTypeId,
@@ -443,6 +448,17 @@ public sealed class TypeMeta : IEquatable<TypeMeta>
 
     public ulong HeaderHash { get; }
 
+    internal void EnsureAssignedFieldIds(IReadOnlyList<TypeMetaFieldInfo> localFieldInfos)
+    {
+        if (_hasAssignedFieldIds)
+        {
+            return;
+        }
+
+        AssignFieldIds(this, localFieldInfos);
+        _hasAssignedFieldIds = true;
+    }
+
     public byte[] Encode()
     {
         if (Compressed)
@@ -553,6 +569,142 @@ public sealed class TypeMeta : IEquatable<TypeMeta>
             header >> (int)(64 - TypeMetaConstants.TypeMetaNumHashBits));
     }
 
+    /// <summary>
+    /// Assigns local sorted field indexes for a remote compatible type meta.
+    /// The result is written to each remote field's <see cref="TypeMetaFieldInfo.AssignedFieldId"/>:
+    /// - local sorted field index when a compatible local field is found
+    /// - -1 when no compatible local field is found and the field should be skipped
+    /// </summary>
+    public static void AssignFieldIds(
+        TypeMeta remoteTypeMeta,
+        IReadOnlyList<TypeMetaFieldInfo> localFieldInfos)
+    {
+        ArgumentNullException.ThrowIfNull(remoteTypeMeta);
+        ArgumentNullException.ThrowIfNull(localFieldInfos);
+
+        Dictionary<string, (int Index, TypeMetaFieldInfo Field)> localByName = new(localFieldInfos.Count, StringComparer.Ordinal);
+        Dictionary<short, (int Index, TypeMetaFieldInfo Field)> localById = new(localFieldInfos.Count);
+        for (int i = 0; i < localFieldInfos.Count; i++)
+        {
+            TypeMetaFieldInfo localField = localFieldInfos[i];
+            if (!string.IsNullOrEmpty(localField.FieldName))
+            {
+                localByName.TryAdd(localField.FieldName, (i, localField));
+            }
+
+            if (localField.FieldId.HasValue && localField.FieldId.Value >= 0)
+            {
+                short fieldId = localField.FieldId.Value;
+                if (!localById.TryAdd(fieldId, (i, localField)))
+                {
+                    throw new InvalidDataException(
+                        $"duplicate local field id {fieldId} in compatible type metadata");
+                }
+            }
+        }
+
+        HashSet<short>? remoteFieldIds = null;
+        for (int i = 0; i < remoteTypeMeta.Fields.Count; i++)
+        {
+            TypeMetaFieldInfo remoteField = remoteTypeMeta.Fields[i];
+            if (remoteField.FieldId.HasValue && remoteField.FieldId.Value >= 0)
+            {
+                short fieldId = remoteField.FieldId.Value;
+                remoteFieldIds ??= [];
+                if (!remoteFieldIds.Add(fieldId))
+                {
+                    throw new InvalidDataException(
+                        $"duplicate remote field id {fieldId} in compatible type metadata");
+                }
+            }
+
+            int localIndex = -1;
+            TypeMetaFieldInfo? localMatch = null;
+
+            if (remoteField.FieldId.HasValue &&
+                localById.TryGetValue(remoteField.FieldId.Value, out (int Index, TypeMetaFieldInfo Field) byId))
+            {
+                localIndex = byId.Index;
+                localMatch = byId.Field;
+            }
+            else
+            {
+                if (localByName.TryGetValue(remoteField.FieldName, out (int Index, TypeMetaFieldInfo Field) byName))
+                {
+                    localIndex = byName.Index;
+                    localMatch = byName.Field;
+                }
+                else
+                {
+                    string normalizedName = TypeMetaUtils.LowerCamelToLowerUnderscore(remoteField.FieldName);
+                    if (!ReferenceEquals(normalizedName, remoteField.FieldName) &&
+                        localByName.TryGetValue(normalizedName, out byName))
+                    {
+                        localIndex = byName.Index;
+                        localMatch = byName.Field;
+                    }
+                }
+            }
+
+            if (localIndex >= 0 &&
+                localMatch is not null &&
+                IsCompatibleFieldType(remoteField.FieldType, localMatch.FieldType))
+            {
+                remoteField.AssignedFieldId = localIndex;
+            }
+            else
+            {
+                remoteField.AssignedFieldId = -1;
+            }
+        }
+    }
+
+    private static bool IsCompatibleFieldType(TypeMetaFieldType remote, TypeMetaFieldType local)
+    {
+        if (NormalizeTypeIdForMatch(remote.TypeId) != NormalizeTypeIdForMatch(local.TypeId))
+        {
+            return false;
+        }
+
+        if (remote.Generics.Count != local.Generics.Count)
+        {
+            return false;
+        }
+
+        for (int i = 0; i < remote.Generics.Count; i++)
+        {
+            if (!IsCompatibleFieldType(remote.Generics[i], local.Generics[i]))
+            {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    private static uint NormalizeTypeIdForMatch(uint typeId)
+    {
+        return typeId switch
+        {
+            (uint)global::Apache.Fory.TypeId.Struct or
+            (uint)global::Apache.Fory.TypeId.CompatibleStruct or
+            (uint)global::Apache.Fory.TypeId.NamedStruct or
+            (uint)global::Apache.Fory.TypeId.NamedCompatibleStruct or
+            (uint)global::Apache.Fory.TypeId.Ext or
+            (uint)global::Apache.Fory.TypeId.NamedExt or
+            (uint)global::Apache.Fory.TypeId.Unknown => (uint)global::Apache.Fory.TypeId.Struct,
+            (uint)global::Apache.Fory.TypeId.Enum or
+            (uint)global::Apache.Fory.TypeId.NamedEnum => (uint)global::Apache.Fory.TypeId.Enum,
+            (uint)global::Apache.Fory.TypeId.Union or
+            (uint)global::Apache.Fory.TypeId.NamedUnion or
+            (uint)global::Apache.Fory.TypeId.TypedUnion => (uint)global::Apache.Fory.TypeId.Union,
+            (uint)global::Apache.Fory.TypeId.Binary or
+            (uint)global::Apache.Fory.TypeId.Int8Array or
+            (uint)global::Apache.Fory.TypeId.UInt8Array => (uint)global::Apache.Fory.TypeId.Binary,
+            _ => typeId,
+        };
+    }
+
     private byte[] EncodeBody()
     {
         ByteWriter writer = new(128);
diff --git a/csharp/src/Fory/TypeResolver.cs b/csharp/src/Fory/TypeResolver.cs
index c938c3052..782213a4c 100644
--- a/csharp/src/Fory/TypeResolver.cs
+++ b/csharp/src/Fory/TypeResolver.cs
@@ -15,16 +15,18 @@
 // specific language governing permissions and limitations
 // under the License.
 
+using System.Buffers.Binary;
 using System.Collections.Concurrent;
 using System.Collections.Immutable;
+using System.Linq;
 using System.Reflection;
+using System.Threading;
 
 namespace Apache.Fory;
 
 public sealed class TypeResolver
 {
     private static readonly ConcurrentDictionary<Type, Func<TypeResolver, TypeInfo>> GeneratedFactories = new();
-    private static readonly ConcurrentDictionary<TypeId, HashSet<TypeId>> SingleAllowedWireTypes = new();
     private static readonly MethodInfo CreateTypeInfoFromSerializerTypedMethod =
         typeof(TypeResolver).GetMethod(
             nameof(CreateTypeInfoFromSerializerTyped),
@@ -33,13 +35,31 @@ public sealed class TypeResolver
         typeof(TypeResolver).GetMethod(
             nameof(CreateNullableSerializerTypeInfo),
             BindingFlags.NonPublic | BindingFlags.Instance)!;
-    private static readonly HashSet<TypeId> CompatibleStructAllowedWireTypes =
-    [
-        TypeId.Struct,
-        TypeId.NamedStruct,
-        TypeId.CompatibleStruct,
-        TypeId.NamedCompatibleStruct,
-    ];
+
+    private readonly record struct CompatibleTypeMetaCacheKey(Type Type, TypeId WireTypeId, bool TrackRef);
+
+    // Cache lookup intentionally keys only by ResolverVersion.
+    // Keep this entry resolver-independent: do not add TypeResolver references here.
+    // ResolverVersion is a deterministic semantic fingerprint only (no resolver identity).
+    // This keeps generic cache reuse efficient across equivalent resolver instances,
+    // including per-thread resolvers in ThreadSafeFory.
+    private sealed class GenericTypeCacheEntry<T>
+    {
+        public GenericTypeCacheEntry(ulong resolverVersion, TypeInfo typeInfo)
+        {
+            ResolverVersion = resolverVersion;
+            TypeInfo = typeInfo;
+        }
+
+        public ulong ResolverVersion { get; }
+
+        public TypeInfo TypeInfo { get; }
+    }
+
+    private static class GenericTypeCache<T>
+    {
+        public static GenericTypeCacheEntry<T>? Entry;
+    }
     private static readonly Dictionary<Type, Type> PrimitiveStringKeyDictionaryCodecs = new()
     {
         [typeof(string)] = typeof(StringPrimitiveDictionaryCodec),
@@ -92,8 +112,12 @@ public sealed class TypeResolver
 
     private readonly Dictionary<uint, TypeInfo> _byUserTypeId = [];
     private readonly Dictionary<(string NamespaceName, string TypeName), TypeInfo> _byTypeName = [];
+    private readonly Dictionary<CompatibleTypeMetaCacheKey, TypeInfo.CompatibleTypeMetaCacheEntry> _compatibleTypeMetaCache = [];
+    private readonly Dictionary<Type, TypeMeta> _validatedCompatibleTypeMetaByType = [];
 
     private readonly Dictionary<Type, TypeInfo> _typeInfos = [];
+    private ulong _versionHash;
+    private bool _finalized;
 
     public static void RegisterGenerated<T, TSerializer>()
         where TSerializer : Serializer<T>, new()
@@ -104,7 +128,18 @@ public sealed class TypeResolver
 
     public Serializer<T> GetSerializer<T>()
     {
-        return GetTypeInfo<T>().RequireSerializer<T>();
+        if (_finalized)
+        {
+            ulong version = _versionHash;
+            GenericTypeCacheEntry<T>? cacheEntry = Volatile.Read(ref GenericTypeCache<T>.Entry);
+            if (cacheEntry is not null && cacheEntry.ResolverVersion == version)
+            {
+                return cacheEntry.TypeInfo.RequireSerializer<T>();
+            }
+        }
+
+        TypeInfo typeInfo = GetTypeInfo<T>();
+        return typeInfo.RequireSerializer<T>();
     }
 
     public TypeInfo GetTypeInfo(Type type)
@@ -114,7 +149,22 @@ public sealed class TypeResolver
 
     public TypeInfo GetTypeInfo<T>()
     {
-        return GetTypeInfo(typeof(T));
+        if (_finalized)
+        {
+            ulong version = _versionHash;
+            GenericTypeCacheEntry<T>? cacheEntry = Volatile.Read(ref GenericTypeCache<T>.Entry);
+            if (cacheEntry is not null && cacheEntry.ResolverVersion == version)
+            {
+                return cacheEntry.TypeInfo;
+            }
+        }
+
+        TypeInfo typeInfo = GetTypeInfo(typeof(T));
+        EnsureFinalizedVersion();
+        Volatile.Write(
+            ref GenericTypeCache<T>.Entry,
+            new GenericTypeCacheEntry<T>(_versionHash, typeInfo));
+        return typeInfo;
     }
 
     internal bool IsNoneObject(TypeInfo typeInfo, object? value)
@@ -192,10 +242,11 @@ public sealed class TypeResolver
 
         if (_typeInfos.TryGetValue(type, out TypeInfo? previous))
         {
-            typeInfo.CopyRegistrationFrom(previous);
+            typeInfo = typeInfo.WithRegistrationFrom(previous);
         }
 
         _typeInfos[type] = typeInfo;
+        InvalidateFinalizedVersion();
         return typeInfo;
     }
 
@@ -214,9 +265,10 @@ public sealed class TypeResolver
 
     internal void Register(Type type, uint id, TypeInfo? explicitTypeInfo = null)
     {
-        TypeInfo typeInfo = GetOrCreateTypeInfo(type, explicitTypeInfo);
-        typeInfo.RegisterByTypeId(id);
+        TypeInfo typeInfo = GetOrCreateTypeInfo(type, explicitTypeInfo).WithTypeIdRegistration(id);
+        _typeInfos[type] = typeInfo;
         _byUserTypeId[id] = typeInfo;
+        InvalidateFinalizedVersion();
     }
 
     internal void Register(Type type, string namespaceName, string typeName, TypeInfo? explicitTypeInfo = null)
@@ -224,8 +276,211 @@ public sealed class TypeResolver
         TypeInfo typeInfo = GetOrCreateTypeInfo(type, explicitTypeInfo);
         MetaString namespaceMeta = MetaStringEncoder.Namespace.Encode(namespaceName, TypeMetaEncodings.NamespaceMetaStringEncodings);
         MetaString typeNameMeta = MetaStringEncoder.TypeName.Encode(typeName, TypeMetaEncodings.TypeNameMetaStringEncodings);
-        typeInfo.RegisterByTypeName(namespaceMeta, typeNameMeta);
+        typeInfo = typeInfo.WithTypeNameRegistration(namespaceMeta, typeNameMeta);
+        _typeInfos[type] = typeInfo;
         _byTypeName[(namespaceName, typeName)] = typeInfo;
+        InvalidateFinalizedVersion();
+    }
+
+    /// <summary>
+    /// Returns a finalized semantic resolver version used by generated/static caches.
+    /// The version is computed lazily and changes whenever bindings/registrations change.
+    /// </summary>
+    /// <returns>Resolver version token.</returns>
+    public ulong VersionHash()
+    {
+        EnsureFinalizedVersion();
+        return _versionHash;
+    }
+
+    /// <summary>
+    /// Returns compatible TypeMeta header hash for a registered struct type.
+    /// </summary>
+    /// <typeparam name="T">Registered struct type.</typeparam>
+    /// <param name="trackRef">Whether the TypeMeta should be generated with tracked references.</param>
+    /// <returns>TypeMeta header hash bits used for exact-schema fast path checks.</returns>
+    public ulong GetCompatibleTypeMetaHeaderHash<T>(bool trackRef)
+    {
+        return GetCompatibleTypeMetaHeaderHash(typeof(T), trackRef);
+    }
+
+    /// <summary>
+    /// Returns compatible TypeMeta header hash for a registered struct type.
+    /// </summary>
+    /// <param name="type">Registered struct type.</param>
+    /// <param name="trackRef">Whether the TypeMeta should be generated with tracked references.</param>
+    /// <returns>TypeMeta header hash bits used for exact-schema fast path checks.</returns>
+    public ulong GetCompatibleTypeMetaHeaderHash(Type type, bool trackRef)
+    {
+        ArgumentNullException.ThrowIfNull(type);
+        EnsureFinalizedVersion();
+
+        TypeInfo info = RequireRegisteredTypeInfo(type);
+        if (!info.UserTypeKind.HasValue || info.UserTypeKind.Value != UserTypeKind.Struct)
+        {
+            throw new InvalidDataException(
+                $"compatible TypeMeta hash is only available for registered struct types, got {type}");
+        }
+
+        TypeId wireTypeId = ResolveWireTypeId(info.UserTypeKind.Value, info.RegisterByName, compatible: true);
+        TypeInfo.CompatibleTypeMetaCacheEntry typeMeta = BuildCompatibleTypeMeta(info, wireTypeId, trackRef);
+        return typeMeta.HeaderHash;
+    }
+
+    private void InvalidateFinalizedVersion()
+    {
+        _finalized = false;
+        _versionHash = 0;
+        _compatibleTypeMetaCache.Clear();
+        _validatedCompatibleTypeMetaByType.Clear();
+    }
+
+    private void EnsureFinalizedVersion()
+    {
+        if (_finalized)
+        {
+            return;
+        }
+
+        _versionHash = ComputeVersionHash();
+        _finalized = true;
+    }
+
+    private ulong ComputeVersionHash()
+    {
+        const ulong offsetBasis = 14695981039346656037UL;
+        const ulong prime = 1099511628211UL;
+
+        static ulong MixByte(ulong hash, byte value)
+        {
+            const ulong p = 1099511628211UL;
+            return (hash ^ value) * p;
+        }
+
+        static ulong MixBool(ulong hash, bool value)
+        {
+            return MixByte(hash, value ? (byte)1 : (byte)0);
+        }
+
+        static ulong MixUInt32(ulong hash, uint value)
+        {
+            hash = MixByte(hash, unchecked((byte)(value & 0xFF)));
+            hash = MixByte(hash, unchecked((byte)((value >> 8) & 0xFF)));
+            hash = MixByte(hash, unchecked((byte)((value >> 16) & 0xFF)));
+            hash = MixByte(hash, unchecked((byte)((value >> 24) & 0xFF)));
+            return hash;
+        }
+
+        static ulong MixUInt64(ulong hash, ulong value)
+        {
+            hash = MixByte(hash, unchecked((byte)(value & 0xFF)));
+            hash = MixByte(hash, unchecked((byte)((value >> 8) & 0xFF)));
+            hash = MixByte(hash, unchecked((byte)((value >> 16) & 0xFF)));
+            hash = MixByte(hash, unchecked((byte)((value >> 24) & 0xFF)));
+            hash = MixByte(hash, unchecked((byte)((value >> 32) & 0xFF)));
+            hash = MixByte(hash, unchecked((byte)((value >> 40) & 0xFF)));
+            hash = MixByte(hash, unchecked((byte)((value >> 48) & 0xFF)));
+            hash = MixByte(hash, unchecked((byte)((value >> 56) & 0xFF)));
+            return hash;
+        }
+
+        static ulong MixString(ulong hash, string? value)
+        {
+            if (value is null)
+            {
+                return MixUInt32(hash, uint.MaxValue);
+            }
+
+            hash = MixUInt32(hash, unchecked((uint)value.Length));
+            for (int i = 0; i < value.Length; i++)
+            {
+                char c = value[i];
+                hash = MixByte(hash, unchecked((byte)(c & 0xFF)));
+                hash = MixByte(hash, unchecked((byte)(c >> 8)));
+            }
+
+            return hash;
+        }
+
+        static ulong MixFieldType(ulong hash, TypeMetaFieldType fieldType)
+        {
+            hash = MixUInt32(hash, fieldType.TypeId);
+            hash = MixBool(hash, fieldType.Nullable);
+            hash = MixBool(hash, fieldType.TrackRef);
+            hash = MixUInt32(hash, unchecked((uint)fieldType.Generics.Count));
+            for (int i = 0; i < fieldType.Generics.Count; i++)
+            {
+                hash = MixFieldType(hash, fieldType.Generics[i]);
+            }
+
+            return hash;
+        }
+
+        ulong hash = offsetBasis;
+        hash = MixBool(hash, true);
+
+        List<TypeInfo> typeInfos = _typeInfos.Values
+            .OrderBy(
+                static info => info.Type.AssemblyQualifiedName ?? info.Type.FullName ?? info.Type.Name,
+                StringComparer.Ordinal)
+            .ToList();
+        hash = MixUInt32(hash, unchecked((uint)typeInfos.Count));
+        foreach (TypeInfo info in typeInfos)
+        {
+            hash = MixString(hash, info.Type.AssemblyQualifiedName ?? info.Type.FullName ?? info.Type.Name);
+            hash = MixString(hash, info.SerializerType.AssemblyQualifiedName ?? info.SerializerType.FullName ?? info.SerializerType.Name);
+            hash = MixUInt32(hash, info.BuiltInTypeId.HasValue ? (uint)info.BuiltInTypeId.Value + 1u : 0u);
+            hash = MixUInt32(hash, info.UserTypeKind.HasValue ? (uint)info.UserTypeKind.Value + 1u : 0u);
+            hash = MixBool(hash, info.IsDynamicType);
+            hash = MixBool(hash, info.IsNullableType);
+            hash = MixBool(hash, info.IsReferenceTrackableType);
+            hash = MixBool(hash, info.SupportsCompatibleReadWithoutTypeMeta);
+            hash = MixBool(hash, info.IsRegistered);
+            hash = MixBool(hash, info.RegisterByName);
+
+            if (info.IsRegistered && info.RegisterByName)
+            {
+                hash = MixString(hash, info.NamespaceName?.Value);
+                hash = MixString(hash, info.TypeName?.Value);
+            }
+            else if (info.IsRegistered)
+            {
+                hash = MixUInt32(hash, info.UserTypeId ?? 0u);
+            }
+
+            if (info.UserTypeKind.HasValue && info.UserTypeKind.Value == UserTypeKind.Struct)
+            {
+                for (int i = 0; i < 2; i++)
+                {
+                    bool trackRef = i == 1;
+                    hash = MixBool(hash, trackRef);
+                    IReadOnlyList<TypeMetaFieldInfo> fields = CompatibleTypeMetaFields(info, trackRef);
+                    hash = MixUInt32(hash, unchecked((uint)fields.Count));
+                    for (int fieldIdx = 0; fieldIdx < fields.Count; fieldIdx++)
+                    {
+                        TypeMetaFieldInfo field = fields[fieldIdx];
+                        hash = MixUInt32(hash, field.FieldId.HasValue ? (uint)field.FieldId.Value + 1u : 0u);
+                        hash = MixString(hash, field.FieldName);
+                        hash = MixFieldType(hash, field.FieldType);
+                    }
+                }
+
+                if (info.IsRegistered)
+                {
+                    TypeId wireTypeId = ResolveWireTypeId(info.UserTypeKind.Value, info.RegisterByName, compatible: true);
+                    hash = MixUInt32(hash, (uint)wireTypeId);
+                    hash = MixUInt64(hash, BuildCompatibleTypeMeta(info, wireTypeId, trackRef: false).HeaderHash);
+                    hash = MixUInt64(hash, BuildCompatibleTypeMeta(info, wireTypeId, trackRef: true).HeaderHash);
+                }
+            }
+        }
+
+        if (hash == 0)
+        {
+            hash = prime;
+        }
+
+        return hash;
     }
 
     internal TypeInfo? GetRegisteredTypeInfo(Type type)
@@ -281,7 +536,12 @@ public sealed class TypeResolver
             throw new InvalidDataException($"type {type} has runtime-only type info");
         }
 
-        TypeInfo info = RequireRegisteredTypeInfo(type);
+        TypeInfo info = typeInfo;
+        if (!info.IsRegistered)
+        {
+            throw new TypeNotRegisteredException($"{type} is not registered");
+        }
+
         if (!info.UserTypeKind.HasValue)
         {
             throw new InvalidDataException($"registered type {type} is not a user type");
@@ -294,8 +554,9 @@ public sealed class TypeResolver
             case TypeId.CompatibleStruct:
             case TypeId.NamedCompatibleStruct:
                 {
-                    TypeMeta typeMeta = BuildCompatibleTypeMeta(info, wireTypeId, context.TrackRef);
-                    context.WriteCompatibleTypeMeta(type, typeMeta);
+                    TypeInfo.CompatibleTypeMetaCacheEntry typeMeta =
+                        BuildCompatibleTypeMeta(info, wireTypeId, context.TrackRef);
+                    context.WriteCompatibleTypeMeta(type, typeMeta.EncodedBytes);
                     return;
                 }
             case TypeId.NamedEnum:
@@ -305,8 +566,9 @@ public sealed class TypeResolver
                 {
                     if (context.Compatible)
                     {
-                        TypeMeta typeMeta = BuildCompatibleTypeMeta(info, wireTypeId, context.TrackRef);
-                        context.WriteCompatibleTypeMeta(type, typeMeta);
+                        TypeInfo.CompatibleTypeMetaCacheEntry typeMeta =
+                            BuildCompatibleTypeMeta(info, wireTypeId, context.TrackRef);
+                        context.WriteCompatibleTypeMeta(type, typeMeta.EncodedBytes);
                     }
                     else
                     {
@@ -367,8 +629,8 @@ public sealed class TypeResolver
 
     private void ReadTypeInfoCore(Type type, TypeInfo typeInfo, ReadContext context)
     {
-        uint rawTypeId = context.Reader.ReadVarUInt32();
-        if (!Enum.IsDefined(typeof(TypeId), rawTypeId))
+        uint rawTypeId = context.Reader.ReadUInt8();
+        if (!IsKnownTypeId(rawTypeId))
         {
             throw new InvalidDataException($"unknown type id {rawTypeId}");
         }
@@ -389,23 +651,47 @@ public sealed class TypeResolver
             throw new InvalidDataException($"type {type} has runtime-only type info");
         }
 
-        TypeInfo info = RequireRegisteredTypeInfo(type);
+        TypeInfo info = typeInfo;
+        if (!info.IsRegistered)
+        {
+            throw new TypeNotRegisteredException($"{type} is not registered");
+        }
+
         if (!info.UserTypeKind.HasValue)
         {
             throw new InvalidDataException($"registered type {type} is not a user type");
         }
 
-        HashSet<TypeId> allowed = AllowedWireTypeIds(info.UserTypeKind.Value, info.RegisterByName, context.Compatible);
-        if (!allowed.Contains(typeId))
+        if (context.Compatible &&
+            info.UserTypeKind.Value == UserTypeKind.Struct &&
+            !info.RegisterByName &&
+            typeId == TypeId.CompatibleStruct)
         {
-            uint expected = 0;
-            foreach (TypeId allowedType in allowed)
+            TypeMeta remoteTypeMeta = context.ReadCompatibleTypeMeta();
+            if (!IsValidatedCompatibleTypeMeta(info, remoteTypeMeta))
             {
-                expected = (uint)allowedType;
-                break;
+                ValidateCompatibleTypeMeta(
+                    remoteTypeMeta,
+                    info,
+                    info.UserTypeKind.Value,
+                    info.RegisterByName,
+                    context.Compatible,
+                    typeId);
+                remoteTypeMeta.EnsureAssignedFieldIds(CompatibleTypeMetaFields(info, context.TrackRef));
+                MarkValidatedCompatibleTypeMeta(info, remoteTypeMeta);
             }
 
-            throw new TypeMismatchException(expected, rawTypeId);
+            context.PushCompatibleTypeMeta(type, remoteTypeMeta);
+            return;
+        }
+
+        UserTypeKind declaredKind = info.UserTypeKind.Value;
+        bool registerByName = info.RegisterByName;
+        bool compatible = context.Compatible;
+        if (!IsAllowedWireType(typeId, declaredKind, registerByName, compatible))
+        {
+            TypeId expected = ResolveWireTypeId(declaredKind, registerByName, compatible);
+            throw new TypeMismatchException((uint)expected, rawTypeId);
         }
 
         switch (typeId)
@@ -414,7 +700,19 @@ public sealed class TypeResolver
             case TypeId.NamedCompatibleStruct:
                 {
                     TypeMeta remoteTypeMeta = context.ReadCompatibleTypeMeta();
-                    ValidateCompatibleTypeMeta(remoteTypeMeta, info, allowed, typeId);
+                    if (!IsValidatedCompatibleTypeMeta(info, remoteTypeMeta))
+                    {
+                        ValidateCompatibleTypeMeta(
+                            remoteTypeMeta,
+                            info,
+                            declaredKind,
+                            registerByName,
+                            compatible,
+                            typeId);
+                        remoteTypeMeta.EnsureAssignedFieldIds(CompatibleTypeMetaFields(info, context.TrackRef));
+                        MarkValidatedCompatibleTypeMeta(info, remoteTypeMeta);
+                    }
+
                     context.PushCompatibleTypeMeta(type, remoteTypeMeta);
                     return;
                 }
@@ -426,7 +724,23 @@ public sealed class TypeResolver
                     if (context.Compatible)
                     {
                         TypeMeta remoteTypeMeta = context.ReadCompatibleTypeMeta();
-                        ValidateCompatibleTypeMeta(remoteTypeMeta, info, allowed, typeId);
+                        if (!IsValidatedCompatibleTypeMeta(info, remoteTypeMeta))
+                        {
+                            ValidateCompatibleTypeMeta(
+                                remoteTypeMeta,
+                                info,
+                                declaredKind,
+                                registerByName,
+                                compatible,
+                                typeId);
+                            if (typeId == TypeId.NamedStruct)
+                            {
+                                remoteTypeMeta.EnsureAssignedFieldIds(CompatibleTypeMetaFields(info, context.TrackRef));
+                            }
+
+                            MarkValidatedCompatibleTypeMeta(info, remoteTypeMeta);
+                        }
+
                         if (typeId == TypeId.NamedStruct)
                         {
                             context.PushCompatibleTypeMeta(type, remoteTypeMeta);
@@ -499,18 +813,34 @@ public sealed class TypeResolver
         };
     }
 
-    internal static HashSet<TypeId> AllowedWireTypeIds(UserTypeKind declaredKind, bool registerByName, bool compatible)
+    private static bool IsKnownTypeId(uint rawTypeId)
+    {
+        return rawTypeId <= (uint)TypeId.Float64Array;
+    }
+
+    private static bool IsAllowedWireType(
+        TypeId actualWireType,
+        UserTypeKind declaredKind,
+        bool registerByName,
+        bool compatible)
     {
         if (declaredKind == UserTypeKind.Struct && compatible)
         {
-            return CompatibleStructAllowedWireTypes;
+            return actualWireType is
+                TypeId.Struct or
+                TypeId.NamedStruct or
+                TypeId.CompatibleStruct or
+                TypeId.NamedCompatibleStruct;
         }
 
         TypeId expected = ResolveWireTypeId(declaredKind, registerByName, compatible);
-        return SingleAllowedWireTypes.GetOrAdd(expected, static typeId => [typeId]);
+        return actualWireType == expected;
     }
 
-    public object? ReadByUserTypeId(uint userTypeId, ReadContext context, TypeMeta? compatibleTypeMeta = null)
+    public object? ReadByUserTypeId(
+        uint userTypeId,
+        ReadContext context,
+        TypeMeta? compatibleTypeMeta = null)
     {
         if (!_byUserTypeId.TryGetValue(userTypeId, out TypeInfo? typeInfo))
         {
@@ -520,7 +850,11 @@ public sealed class TypeResolver
         return ReadRegisteredValue(typeInfo, context, compatibleTypeMeta);
     }
 
-    public object? ReadByTypeName(string namespaceName, string typeName, ReadContext context, TypeMeta? compatibleTypeMeta = null)
+    public object? ReadByTypeName(
+        string namespaceName,
+        string typeName,
+        ReadContext context,
+        TypeMeta? compatibleTypeMeta = null)
     {
         if (!_byTypeName.TryGetValue((namespaceName, typeName), out TypeInfo? typeInfo))
         {
@@ -530,10 +864,14 @@ public sealed class TypeResolver
         return ReadRegisteredValue(typeInfo, context, compatibleTypeMeta);
     }
 
-    private object? ReadRegisteredValue(TypeInfo typeInfo, ReadContext context, TypeMeta? compatibleTypeMeta)
+    private object? ReadRegisteredValue(
+        TypeInfo typeInfo,
+        ReadContext context,
+        TypeMeta? compatibleTypeMeta)
     {
         if (compatibleTypeMeta is not null)
         {
+            compatibleTypeMeta.EnsureAssignedFieldIds(CompatibleTypeMetaFields(typeInfo, context.TrackRef));
             context.PushCompatibleTypeMeta(typeInfo.Type, compatibleTypeMeta);
         }
 
@@ -542,8 +880,8 @@ public sealed class TypeResolver
 
     public DynamicTypeInfo ReadDynamicTypeInfo(ReadContext context)
     {
-        uint rawTypeId = context.Reader.ReadVarUInt32();
-        if (!Enum.IsDefined(typeof(TypeId), rawTypeId))
+        uint rawTypeId = context.Reader.ReadUInt8();
+        if (!IsKnownTypeId(rawTypeId))
         {
             throw new InvalidDataException($"unknown dynamic type id {rawTypeId}");
         }
@@ -557,7 +895,12 @@ public sealed class TypeResolver
                     TypeMeta typeMeta = context.ReadCompatibleTypeMeta();
                     if (typeMeta.RegisterByName)
                     {
-                        return new DynamicTypeInfo(wireTypeId, null, typeMeta.NamespaceName, typeMeta.TypeName, typeMeta);
+                        return new DynamicTypeInfo(
+                            wireTypeId,
+                            null,
+                            typeMeta.NamespaceName,
+                            typeMeta.TypeName,
+                            typeMeta);
                     }
 
                     return new DynamicTypeInfo(wireTypeId, typeMeta.UserTypeId, null, null, typeMeta);
@@ -707,7 +1050,10 @@ public sealed class TypeResolver
                         throw new InvalidDataException("missing user type id in compatible dynamic type meta");
                     }
 
-                    return ReadByUserTypeId(compatibleTypeMeta.UserTypeId.Value, context, compatibleTypeMeta);
+                    return ReadByUserTypeId(
+                        compatibleTypeMeta.UserTypeId.Value,
+                        context,
+                        compatibleTypeMeta);
                 }
             case TypeId.None:
                 return null;
@@ -887,7 +1233,38 @@ public sealed class TypeResolver
         return typeId is TypeId.Enum or TypeId.Struct or TypeId.Ext or TypeId.TypedUnion;
     }
 
-    private TypeMeta BuildCompatibleTypeMeta(
+    private bool IsValidatedCompatibleTypeMeta(TypeInfo info, TypeMeta remoteTypeMeta)
+    {
+        return _validatedCompatibleTypeMetaByType.TryGetValue(info.Type, out TypeMeta? validated) &&
+               ReferenceEquals(validated, remoteTypeMeta);
+    }
+
+    private void MarkValidatedCompatibleTypeMeta(TypeInfo info, TypeMeta remoteTypeMeta)
+    {
+        _validatedCompatibleTypeMetaByType[info.Type] = remoteTypeMeta;
+    }
+
+    private TypeInfo.CompatibleTypeMetaCacheEntry BuildCompatibleTypeMeta(
+        TypeInfo info,
+        TypeId wireTypeId,
+        bool trackRef)
+    {
+        CompatibleTypeMetaCacheKey key = new(info.Type, wireTypeId, trackRef);
+        if (_compatibleTypeMetaCache.TryGetValue(key, out TypeInfo.CompatibleTypeMetaCacheEntry cached))
+        {
+            return cached;
+        }
+
+        TypeMeta typeMeta = BuildCompatibleTypeMetaUncached(info, wireTypeId, trackRef);
+        byte[] encoded = typeMeta.Encode();
+        ulong header = BinaryPrimitives.ReadUInt64LittleEndian(encoded);
+        ulong headerHash = header >> (int)(64 - TypeMetaConstants.TypeMetaNumHashBits);
+        TypeInfo.CompatibleTypeMetaCacheEntry entry = new(typeMeta, encoded, headerHash);
+        _compatibleTypeMetaCache[key] = entry;
+        return entry;
+    }
+
+    private TypeMeta BuildCompatibleTypeMetaUncached(
         TypeInfo info,
         TypeId wireTypeId,
         bool trackRef)
@@ -929,7 +1306,9 @@ public sealed class TypeResolver
     private static void ValidateCompatibleTypeMeta(
         TypeMeta remoteTypeMeta,
         TypeInfo localInfo,
-        HashSet<TypeId> expectedWireTypes,
+        UserTypeKind declaredKind,
+        bool registerByName,
+        bool compatible,
         TypeId actualWireTypeId)
     {
         if (remoteTypeMeta.RegisterByName)
@@ -977,10 +1356,10 @@ public sealed class TypeResolver
         }
 
         if (remoteTypeMeta.TypeId.HasValue &&
-            Enum.IsDefined(typeof(TypeId), remoteTypeMeta.TypeId.Value))
+            IsKnownTypeId(remoteTypeMeta.TypeId.Value))
         {
             TypeId remoteWireTypeId = (TypeId)remoteTypeMeta.TypeId.Value;
-            if (!expectedWireTypes.Contains(remoteWireTypeId))
+            if (!IsAllowedWireType(remoteWireTypeId, declaredKind, registerByName, compatible))
             {
                 throw new TypeMismatchException((uint)actualWireTypeId, remoteTypeMeta.TypeId.Value);
             }
diff --git a/csharp/tests/Fory.Tests/ForyRuntimeTests.cs b/csharp/tests/Fory.Tests/ForyRuntimeTests.cs
index ccc3ccafb..e2fd69c14 100644
--- a/csharp/tests/Fory.Tests/ForyRuntimeTests.cs
+++ b/csharp/tests/Fory.Tests/ForyRuntimeTests.cs
@@ -91,6 +91,18 @@ public sealed class TwoStringField
     public string F2 { get; set; } = string.Empty;
 }
 
+[ForyObject]
+public sealed class OneStringFieldListHolder
+{
+    public List<OneStringField?> Items { get; set; } = [];
+}
+
+[ForyObject]
+public sealed class TwoStringFieldListHolder
+{
+    public List<TwoStringField?> Items { get; set; } = [];
+}
+
 [ForyObject]
 public sealed class StructWithEnum
 {
@@ -721,6 +733,92 @@ public sealed class ForyRuntimeTests
         Assert.Equal(string.Empty, evolved.F2);
     }
 
+    [Theory]
+    [InlineData(false)]
+    [InlineData(true)]
+    public void CompatibleSchemaEvolutionRoundTripForListElements(bool trackRef)
+    {
+        ForyRuntime writer = ForyRuntime.Builder().Compatible(true).TrackRef(trackRef).Build();
+        writer.Register<OneStringField>(200);
+
+        ForyRuntime reader = ForyRuntime.Builder().Compatible(true).TrackRef(trackRef).Build();
+        reader.Register<TwoStringField>(200);
+
+        List<OneStringField?> source =
+        [
+            new OneStringField { F1 = "hello" },
+            null,
+            new OneStringField { F1 = "world" },
+        ];
+
+        byte[] payload = writer.Serialize(source);
+        List<TwoStringField?> evolved = reader.Deserialize<List<TwoStringField?>>(payload);
+
+        Assert.Equal(3, evolved.Count);
+        TwoStringField first = Assert.IsType<TwoStringField>(evolved[0]);
+        Assert.Equal("hello", first.F1);
+        Assert.Equal(string.Empty, first.F2);
+        Assert.Null(evolved[1]);
+        TwoStringField third = Assert.IsType<TwoStringField>(evolved[2]);
+        Assert.Equal("world", third.F1);
+        Assert.Equal(string.Empty, third.F2);
+
+        first.F2 = "extra-first";
+        third.F2 = "extra-third";
+        List<OneStringField?> roundTripped = writer.Deserialize<List<OneStringField?>>(reader.Serialize(evolved));
+
+        Assert.Equal(3, roundTripped.Count);
+        OneStringField firstRound = Assert.IsType<OneStringField>(roundTripped[0]);
+        Assert.Equal("hello", firstRound.F1);
+        Assert.Null(roundTripped[1]);
+        OneStringField thirdRound = Assert.IsType<OneStringField>(roundTripped[2]);
+        Assert.Equal("world", thirdRound.F1);
+    }
+
+    [Theory]
+    [InlineData(false)]
+    [InlineData(true)]
+    public void CompatibleSchemaEvolutionRoundTripForNestedListField(bool trackRef)
+    {
+        ForyRuntime writer = ForyRuntime.Builder().Compatible(true).TrackRef(trackRef).Build();
+        writer.Register<OneStringField>(200);
+        writer.Register<OneStringFieldListHolder>(201);
+
+        ForyRuntime reader = ForyRuntime.Builder().Compatible(true).TrackRef(trackRef).Build();
+        reader.Register<TwoStringField>(200);
+        reader.Register<TwoStringFieldListHolder>(201);
+
+        OneStringFieldListHolder source = new()
+        {
+            Items =
+            [
+                new OneStringField { F1 = "hello" },
+                null,
+                new OneStringField { F1 = "world" },
+            ],
+        };
+
+        TwoStringFieldListHolder evolved = reader.Deserialize<TwoStringFieldListHolder>(writer.Serialize(source));
+        Assert.Equal(3, evolved.Items.Count);
+        TwoStringField first = Assert.IsType<TwoStringField>(evolved.Items[0]);
+        Assert.Equal("hello", first.F1);
+        Assert.Equal(string.Empty, first.F2);
+        Assert.Null(evolved.Items[1]);
+        TwoStringField third = Assert.IsType<TwoStringField>(evolved.Items[2]);
+        Assert.Equal("world", third.F1);
+        Assert.Equal(string.Empty, third.F2);
+
+        first.F2 = "extra-first";
+        third.F2 = "extra-third";
+        OneStringFieldListHolder roundTripped = writer.Deserialize<OneStringFieldListHolder>(reader.Serialize(evolved));
+        Assert.Equal(3, roundTripped.Items.Count);
+        OneStringField firstRound = Assert.IsType<OneStringField>(roundTripped.Items[0]);
+        Assert.Equal("hello", firstRound.F1);
+        Assert.Null(roundTripped.Items[1]);
+        OneStringField thirdRound = Assert.IsType<OneStringField>(roundTripped.Items[2]);
+        Assert.Equal("world", thirdRound.F1);
+    }
+
     [Fact]
     public void SchemaVersionMismatchThrows()
     {
@@ -930,6 +1028,220 @@ public sealed class ForyRuntimeTests
         Assert.Equal(value, decoded);
     }
 
+    [Fact]
+    public void TypeResolverVersionHashIncludesUnregisteredTypeBindings()
+    {
+        TypeResolver resolver = new();
+        ulong baselineHash = resolver.VersionHash();
+        _ = resolver.GetTypeInfo<List<int>>();
+        ulong boundHash = resolver.VersionHash();
+
+        Assert.NotEqual(baselineHash, boundHash);
+    }
+
+    [Fact]
+    public void TypeResolverVersionHashIsStableWithinSameFinalizedResolver()
+    {
+        TypeResolver resolver = new();
+        _ = resolver.GetTypeInfo<List<int>>();
+        _ = resolver.GetTypeInfo<Dictionary<string, int>>();
+
+        ulong first = resolver.VersionHash();
+        ulong second = resolver.VersionHash();
+
+        Assert.Equal(first, second);
+    }
+
+    [Fact]
+    public void TypeResolverVersionHashIsDeterministicForSameBoundTypes()
+    {
+        TypeResolver resolverA = new();
+        _ = resolverA.GetTypeInfo<List<int>>();
+        _ = resolverA.GetTypeInfo<Dictionary<string, int>>();
+        ulong hashA = resolverA.VersionHash();
+
+        TypeResolver resolverB = new();
+        _ = resolverB.GetTypeInfo<List<int>>();
+        _ = resolverB.GetTypeInfo<Dictionary<string, int>>();
+        ulong hashB = resolverB.VersionHash();
+
+        Assert.Equal(hashA, hashB);
+    }
+
+    [Fact]
+    public void CompatibleStructFastPathValidatesEmbeddedTypeMetaTypeId()
+    {
+        ForyRuntime writer = ForyRuntime.Builder().Compatible(true).Build();
+        writer.Register<OneStringField>(200);
+        byte[] payload = writer.Serialize(new OneStringField { F1 = "hello" });
+
+        byte[] tamperedPayload = RewriteCompatibleTypeMetaTypeId(payload, (uint)TypeId.Map);
+
+        ForyRuntime reader = ForyRuntime.Builder().Compatible(true).Build();
+        reader.Register<OneStringField>(200);
+        Assert.Throws<TypeMismatchException>(() => reader.Deserialize<OneStringField>(tamperedPayload));
+    }
+
+    [Fact]
+    public void TypeMetaAssignFieldIdsPrefersIdAndFallsBackToName()
+    {
+        List<TypeMetaFieldInfo> localFields =
+        [
+            new TypeMetaFieldInfo(1, "int_value", new TypeMetaFieldType((uint)TypeId.VarInt32, false)),
+            new TypeMetaFieldInfo(2, "name", new TypeMetaFieldType((uint)TypeId.String, true)),
+        ];
+        List<TypeMetaFieldInfo> remoteFields =
+        [
+            new TypeMetaFieldInfo(2, "$tag2", new TypeMetaFieldType((uint)TypeId.String, true)),
+            new TypeMetaFieldInfo(null, "intValue", new TypeMetaFieldType((uint)TypeId.VarInt32, false)),
+            new TypeMetaFieldInfo(99, "$tag99", new TypeMetaFieldType((uint)TypeId.String, true)),
+        ];
+        TypeMeta remoteTypeMeta = new(
+            (uint)TypeId.CompatibleStruct,
+            500,
+            MetaString.Empty('.', '_'),
+            MetaString.Empty('$', '_'),
+            registerByName: false,
+            remoteFields);
+
+        TypeMeta.AssignFieldIds(remoteTypeMeta, localFields);
+        Assert.Equal(1, remoteTypeMeta.Fields[0].AssignedFieldId);
+        Assert.Equal(0, remoteTypeMeta.Fields[1].AssignedFieldId);
+        Assert.Equal(-1, remoteTypeMeta.Fields[2].AssignedFieldId);
+    }
+
+    [Fact]
+    public void TypeMetaAssignFieldIdsSkipsTypeMismatchedField()
+    {
+        List<TypeMetaFieldInfo> localFields =
+        [
+            new TypeMetaFieldInfo(1, "value", new TypeMetaFieldType((uint)TypeId.VarInt32, false)),
+        ];
+        List<TypeMetaFieldInfo> remoteFields =
+        [
+            new TypeMetaFieldInfo(1, "$tag1", new TypeMetaFieldType((uint)TypeId.String, false)),
+        ];
+        TypeMeta remoteTypeMeta = new(
+            (uint)TypeId.CompatibleStruct,
+            501,
+            MetaString.Empty('.', '_'),
+            MetaString.Empty('$', '_'),
+            registerByName: false,
+            remoteFields);
+
+        TypeMeta.AssignFieldIds(remoteTypeMeta, localFields);
+        Assert.Equal(-1, remoteTypeMeta.Fields[0].AssignedFieldId);
+    }
+
+    [Fact]
+    public void TypeMetaAssignFieldIdsNormalizesStructLikeTypeIds()
+    {
+        List<TypeMetaFieldInfo> localFields =
+        [
+            new TypeMetaFieldInfo(1, "payload", new TypeMetaFieldType((uint)TypeId.Struct, true)),
+        ];
+        List<TypeMetaFieldInfo> remoteFields =
+        [
+            new TypeMetaFieldInfo(1, "$tag1", new TypeMetaFieldType((uint)TypeId.Unknown, true)),
+        ];
+        TypeMeta remoteTypeMeta = new(
+            (uint)TypeId.CompatibleStruct,
+            502,
+            MetaString.Empty('.', '_'),
+            MetaString.Empty('$', '_'),
+            registerByName: false,
+            remoteFields);
+
+        TypeMeta.AssignFieldIds(remoteTypeMeta, localFields);
+        Assert.Equal(0, remoteTypeMeta.Fields[0].AssignedFieldId);
+    }
+
+    [Fact]
+    public void TypeMetaAssignFieldIdsThrowsOnDuplicateLocalFieldId()
+    {
+        List<TypeMetaFieldInfo> localFields =
+        [
+            new TypeMetaFieldInfo(7, "first", new TypeMetaFieldType((uint)TypeId.VarInt32, false)),
+            new TypeMetaFieldInfo(7, "second", new TypeMetaFieldType((uint)TypeId.VarInt32, false)),
+        ];
+        List<TypeMetaFieldInfo> remoteFields =
+        [
+            new TypeMetaFieldInfo(7, "$tag7", new TypeMetaFieldType((uint)TypeId.VarInt32, false)),
+        ];
+        TypeMeta remoteTypeMeta = new(
+            (uint)TypeId.CompatibleStruct,
+            503,
+            MetaString.Empty('.', '_'),
+            MetaString.Empty('$', '_'),
+            registerByName: false,
+            remoteFields);
+
+        InvalidDataException exception = Assert.Throws<InvalidDataException>(
+            () => TypeMeta.AssignFieldIds(remoteTypeMeta, localFields));
+        Assert.Contains("duplicate local field id 7", exception.Message, StringComparison.Ordinal);
+    }
+
+    [Fact]
+    public void TypeMetaAssignFieldIdsThrowsOnDuplicateRemoteFieldId()
+    {
+        List<TypeMetaFieldInfo> localFields =
+        [
+            new TypeMetaFieldInfo(9, "value", new TypeMetaFieldType((uint)TypeId.VarInt32, false)),
+        ];
+        List<TypeMetaFieldInfo> remoteFields =
+        [
+            new TypeMetaFieldInfo(9, "$tag9a", new TypeMetaFieldType((uint)TypeId.VarInt32, false)),
+            new TypeMetaFieldInfo(9, "$tag9b", new TypeMetaFieldType((uint)TypeId.VarInt32, false)),
+        ];
+        TypeMeta remoteTypeMeta = new(
+            (uint)TypeId.CompatibleStruct,
+            504,
+            MetaString.Empty('.', '_'),
+            MetaString.Empty('$', '_'),
+            registerByName: false,
+            remoteFields);
+
+        InvalidDataException exception = Assert.Throws<InvalidDataException>(
+            () => TypeMeta.AssignFieldIds(remoteTypeMeta, localFields));
+        Assert.Contains("duplicate remote field id 9", exception.Message, StringComparison.Ordinal);
+    }
+
+    private static byte[] RewriteCompatibleTypeMetaTypeId(byte[] payload, uint embeddedTypeId)
+    {
+        ByteReader reader = new(payload);
+        _ = reader.ReadUInt8(); // frame header bitmap
+
+        sbyte refFlag = reader.ReadInt8();
+        Assert.Equal((sbyte)RefFlag.NotNullValue, refFlag);
+
+        uint wireTypeId = reader.ReadUInt8();
+        Assert.Equal((uint)TypeId.CompatibleStruct, wireTypeId);
+
+        uint typeMetaIndexMarker = reader.ReadVarUInt32();
+        Assert.Equal(0u, typeMetaIndexMarker & 1u);
+
+        int typeMetaStart = reader.Cursor;
+        TypeMeta originalTypeMeta = TypeMeta.Decode(reader);
+        int typeMetaEnd = reader.Cursor;
+
+        TypeMeta rewrittenTypeMeta = new(
+            embeddedTypeId,
+            originalTypeMeta.UserTypeId,
+            originalTypeMeta.NamespaceName,
+            originalTypeMeta.TypeName,
+            originalTypeMeta.RegisterByName,
+            originalTypeMeta.Fields,
+            originalTypeMeta.HasFieldsMeta,
+            originalTypeMeta.Compressed);
+        byte[] rewrittenTypeMetaBytes = rewrittenTypeMeta.Encode();
+
+        byte[] rewrittenPayload = new byte[typeMetaStart + rewrittenTypeMetaBytes.Length + (payload.Length - typeMetaEnd)];
+        Buffer.BlockCopy(payload, 0, rewrittenPayload, 0, typeMetaStart);
+        Buffer.BlockCopy(rewrittenTypeMetaBytes, 0, rewrittenPayload, typeMetaStart, rewrittenTypeMetaBytes.Length);
+        Buffer.BlockCopy(payload, typeMetaEnd, rewrittenPayload, typeMetaStart + rewrittenTypeMetaBytes.Length, payload.Length - typeMetaEnd);
+        return rewrittenPayload;
+    }
+
     private static (ulong Encoding, string Decoded) WriteAndReadString(string value)
     {
         ByteWriter writer = new();
diff --git a/docs/benchmarks/csharp/README.md b/docs/benchmarks/csharp/README.md
new file mode 100644
index 000000000..f453949d8
--- /dev/null
+++ b/docs/benchmarks/csharp/README.md
@@ -0,0 +1,129 @@
+# C# Benchmark Performance Report
+
+_Generated on 2026-03-04 10:55:09_
+
+## How to Generate This Report
+
+```bash
+cd benchmarks/csharp
+dotnet run -c Release --project ./Fory.CSharpBenchmark.csproj -- --output build/benchmark_results.json
+python3 benchmark_report.py --json-file build/benchmark_results.json --output-dir report
+```
+
+## Hardware & OS Info
+
+| Key                                | Value                                                                                                                        |
+| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
+| OS                                 | Darwin 24.6.0 Darwin Kernel Version 24.6.0: Wed Oct 15 21:12:15 PDT 2025; root:xnu-11417.140.69.703.14~1/RELEASE_ARM64_T6041 |
+| OS Architecture                    | Arm64                                                                                                                        |
+| Machine                            | Arm64                                                                                                                        |
+| Runtime Version                    | 8.0.24                                                                                                                       |
+| Benchmark Date (UTC)               | 2026-03-04T02:49:28.6972030Z                                                                                                 |
+| Warmup Seconds                     | 1                                                                                                                            |
+| Duration Seconds                   | 3                                                                                                                            |
+| CPU Logical Cores (from benchmark) | 12                                                                                                                           |
+| CPU Cores (Physical)               | 12                                                                                                                           |
+| CPU Cores (Logical)                | 12                                                                                                                           |
+| Total RAM (GB)                     | 48.0                                                                                                                         |
+
+## Benchmark Coverage
+
+| Key                 | Value                                                                  |
+| ------------------- | ---------------------------------------------------------------------- |
+| Cases in input JSON | 36 / 36                                                                |
+| Serializers         | fory, msgpack, protobuf                                                |
+| Datatypes           | struct, sample, mediacontent, structlist, samplelist, mediacontentlist |
+| Operations          | serialize, deserialize                                                 |
+
+## Benchmark Plots
+
+All class-level plots below show throughput (ops/sec).
+
+### Throughput
+
+<p align="center">
+<img src="throughput.png" width="90%" />
+</p>
+
+### Mediacontent
+
+<p align="center">
+<img src="mediacontent.png" width="90%" />
+</p>
+
+### Mediacontentlist
+
+<p align="center">
+<img src="mediacontentlist.png" width="90%" />
+</p>
+
+### Sample
+
+<p align="center">
+<img src="sample.png" width="90%" />
+</p>
+
+### Samplelist
+
+<p align="center">
+<img src="samplelist.png" width="90%" />
+</p>
+
+### Struct
+
+<p align="center">
+<img src="struct.png" width="90%" />
+</p>
+
+### Structlist
+
+<p align="center">
+<img src="structlist.png" width="90%" />
+</p>
+
+## Benchmark Results
+
+### Timing Results (nanoseconds)
+
+| Datatype         | Operation   | fory (ns) | protobuf (ns) | msgpack (ns) | Fastest |
+| ---------------- | ----------- | --------- | ------------- | ------------ | ------- |
+| Struct           | Serialize   | 47.8      | 129.5         | 66.9         | fory    |
+| Struct           | Deserialize | 55.2      | 195.6         | 103.5        | fory    |
+| Sample           | Serialize   | 307.7     | 570.8         | 345.0        | fory    |
+| Sample           | Deserialize | 174.7     | 1150.5        | 561.7        | fory    |
+| MediaContent     | Serialize   | 289.6     | 481.0         | 360.2        | fory    |
+| MediaContent     | Deserialize | 343.2     | 779.3         | 732.0        | fory    |
+| StructList       | Serialize   | 127.2     | 477.2         | 272.2        | fory    |
+| StructList       | Deserialize | 205.1     | 698.3         | 502.7        | fory    |
+| SampleList       | Serialize   | 1268.6    | 2813.2        | 1685.8       | fory    |
+| SampleList       | Deserialize | 868.8     | 5590.2        | 2736.3       | fory    |
+| MediaContentList | Serialize   | 1320.5    | 2362.1        | 1689.8       | fory    |
+| MediaContentList | Deserialize | 1621.1    | 3612.8        | 3617.5       | fory    |
+
+### Throughput Results (ops/sec)
+
+| Datatype         | Operation   | fory TPS   | protobuf TPS | msgpack TPS | Fastest |
+| ---------------- | ----------- | ---------- | ------------ | ----------- | ------- |
+| Struct           | Serialize   | 20,918,778 | 7,720,907    | 14,945,879  | fory    |
+| Struct           | Deserialize | 18,124,445 | 5,113,717    | 9,663,285   | fory    |
+| Sample           | Serialize   | 3,250,380  | 1,752,019    | 2,898,164   | fory    |
+| Sample           | Deserialize | 5,724,338  | 869,217      | 1,780,261   | fory    |
+| MediaContent     | Serialize   | 3,452,789  | 2,079,167    | 2,776,587   | fory    |
+| MediaContent     | Deserialize | 2,913,564  | 1,283,181    | 1,366,101   | fory    |
+| StructList       | Serialize   | 7,858,803  | 2,095,745    | 3,673,471   | fory    |
+| StructList       | Deserialize | 4,874,887  | 1,432,142    | 1,989,397   | fory    |
+| SampleList       | Serialize   | 788,246    | 355,470      | 593,176     | fory    |
+| SampleList       | Deserialize | 1,150,990  | 178,885      | 365,453     | fory    |
+| MediaContentList | Serialize   | 757,278    | 423,351      | 591,774     | fory    |
+| MediaContentList | Deserialize | 616,871    | 276,793      | 276,437     | fory    |
+
+### Serialized Data Sizes (bytes)
+
+| Datatype         | fory | protobuf | msgpack |
+| ---------------- | ---- | -------- | ------- |
+| Struct           | 58   | 61       | 55      |
+| Sample           | 446  | 460      | 562     |
+| MediaContent     | 304  | 307      | 479     |
+| StructList       | 155  | 315      | 284     |
+| SampleList       | 1915 | 2315     | 2819    |
+| MediaContentList | 1440 | 1550     | 2404    |
diff --git a/docs/benchmarks/csharp/mediacontent.png b/docs/benchmarks/csharp/mediacontent.png
new file mode 100644
index 000000000..2a4fa9141
Binary files /dev/null and b/docs/benchmarks/csharp/mediacontent.png differ
diff --git a/docs/benchmarks/csharp/mediacontentlist.png b/docs/benchmarks/csharp/mediacontentlist.png
new file mode 100644
index 000000000..9548960df
Binary files /dev/null and b/docs/benchmarks/csharp/mediacontentlist.png differ
diff --git a/docs/benchmarks/csharp/sample.png b/docs/benchmarks/csharp/sample.png
new file mode 100644
index 000000000..4738096f5
Binary files /dev/null and b/docs/benchmarks/csharp/sample.png differ
diff --git a/docs/benchmarks/csharp/samplelist.png b/docs/benchmarks/csharp/samplelist.png
new file mode 100644
index 000000000..e8d3f2b82
Binary files /dev/null and b/docs/benchmarks/csharp/samplelist.png differ
diff --git a/docs/benchmarks/csharp/struct.png b/docs/benchmarks/csharp/struct.png
new file mode 100644
index 000000000..dacd08e8c
Binary files /dev/null and b/docs/benchmarks/csharp/struct.png differ
diff --git a/docs/benchmarks/csharp/structlist.png b/docs/benchmarks/csharp/structlist.png
new file mode 100644
index 000000000..05900416b
Binary files /dev/null and b/docs/benchmarks/csharp/structlist.png differ
diff --git a/docs/benchmarks/csharp/throughput.png b/docs/benchmarks/csharp/throughput.png
new file mode 100644
index 000000000..61ef5dcbe
Binary files /dev/null and b/docs/benchmarks/csharp/throughput.png differ


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@fory.apache.org
For additional commands, e-mail: commits-help@fory.apache.org