diff --git a/AyCode.Benchmark/JitDisassemblyBenchmark.cs b/AyCode.Benchmark/JitDisassemblyBenchmark.cs
index 69d7828..0c77de2 100644
--- a/AyCode.Benchmark/JitDisassemblyBenchmark.cs
+++ b/AyCode.Benchmark/JitDisassemblyBenchmark.cs
@@ -26,7 +26,9 @@ public class JitDisassemblyBenchmark
{
private TestOrder _order = null!;
private AcBinarySerializerOptions _fastModeOptions = null!;
- private byte[] _serialized = null!;
+ private AcBinarySerializerOptions _defaultOptions = null!;
+ private byte[] _serializedFastMode = null!;
+ private byte[] _serializedDefault = null!;
[GlobalSetup]
public void Setup()
@@ -45,12 +47,13 @@ public class JitDisassemblyBenchmark
sharedUser: sharedUser);
_fastModeOptions = AcBinarySerializerOptions.FastMode;
- _serialized = AcBinarySerializer.Serialize(_order, _fastModeOptions);
+ _defaultOptions = AcBinarySerializerOptions.Default;
+ _serializedFastMode = AcBinarySerializer.Serialize(_order, _fastModeOptions);
+ _serializedDefault = AcBinarySerializer.Serialize(_order, _defaultOptions);
}
///
- /// FastMode serialize — the primary hot path.
- /// Disassembly will show WriteObject → WritePropertyMarkerless/WritePropertyOrSkip call chain.
+ /// FastMode serialize — no ref tracking, no string interning.
///
[Benchmark(Baseline = true)]
public byte[] Serialize_FastMode()
@@ -59,11 +62,30 @@ public class JitDisassemblyBenchmark
}
///
- /// FastMode deserialize — for comparison.
+ /// FastMode deserialize.
///
[Benchmark]
public TestOrder Deserialize_FastMode()
{
- return AcBinaryDeserializer.Deserialize(_serialized, _fastModeOptions);
+ return AcBinaryDeserializer.Deserialize(_serializedFastMode, _fastModeOptions);
+ }
+
+ ///
+ /// Default serialize — ref tracking + string interning (scan pass + write pass).
+ /// Shows IdentityMap lookup overhead in hot path.
+ ///
+ [Benchmark]
+ public byte[] Serialize_Default()
+ {
+ return AcBinarySerializer.Serialize(_order, _defaultOptions);
+ }
+
+ ///
+ /// Default deserialize — ref tracking + string interning.
+ ///
+ [Benchmark]
+ public TestOrder Deserialize_Default()
+ {
+ return AcBinaryDeserializer.Deserialize(_serializedDefault, _defaultOptions);
}
}
diff --git a/AyCode.Core.Serializers.Console/Program.cs b/AyCode.Core.Serializers.Console/Program.cs
index 04309a4..77ef1df 100644
--- a/AyCode.Core.Serializers.Console/Program.cs
+++ b/AyCode.Core.Serializers.Console/Program.cs
@@ -212,15 +212,15 @@ public static class Program
{
// AcBinary variants
- //new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.Default, SerializerAcBinaryDefault),
- //new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.WithoutReferenceHandling, SerializerAcBinaryNoRef),
- //new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryFastMode),
- //new AcBinaryBenchmark(testData.Order, new AcBinarySerializerOptions { UseStringInterning = StringInterningMode.None }, SerializerAcBinaryNoIntern),
-
- new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryDefault),
- new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryNoRef),
+ new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.Default, SerializerAcBinaryDefault),
+ new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.WithoutReferenceHandling, SerializerAcBinaryNoRef),
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryFastMode),
- new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryNoIntern),
+ new AcBinaryBenchmark(testData.Order, new AcBinarySerializerOptions { UseStringInterning = StringInterningMode.None }, SerializerAcBinaryNoIntern),
+
+ //new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryDefault),
+ //new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryNoRef),
+ //new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryFastMode),
+ //new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryNoIntern),
// AcJson
new AcJsonBenchmark(testData.Order, AcJsonSerializerOptions.Default, SerializerAcJsonDefault),
diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs
index ca66ad8..b4cf08f 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs
@@ -92,6 +92,85 @@ public static partial class AcBinarySerializer
private int _nextCacheIndex; // Next dense cache index to assign (starts at 0, uses ++_nextCacheIndex)
public int NextFirstIndex; // Next first occurrence index for scan pass. Direct access for performance.
+ #region WriteDuplicateEntry — scan pass output for write pass cursor
+
+ private WriteDuplicateEntry[]? _writePlan;
+ private int _writePlanCount;
+
+ /// Unified scan visit counter. Increments on every IId object and internable string visit.
+ internal int ScanVisitIndex;
+
+ /// Write plan entry count for write pass cursor.
+ internal int WritePlanCount => _writePlanCount;
+
+ /// Write plan array for write pass cursor. Sorted by VisitIndex after scan pass.
+ internal WriteDuplicateEntry[]? WritePlan => _writePlan;
+
+ ///
+ /// Adds a pre-computed write instruction for a duplicate string or IId object reference.
+ ///
+ internal void AddWriteDuplicateEntry(int visitIndex, int cacheMapIndex, bool isFirst, string? value)
+ {
+ if (_writePlan == null)
+ {
+ _writePlan = ArrayPool.Shared.Rent(16);
+ }
+ else if (_writePlanCount >= _writePlan.Length)
+ {
+ var newArray = ArrayPool.Shared.Rent(_writePlan.Length * 2);
+ _writePlan.AsSpan(0, _writePlanCount).CopyTo(newArray);
+ ArrayPool.Shared.Return(_writePlan, clearArray: true);
+ _writePlan = newArray;
+ }
+
+ ref var entry = ref _writePlan[_writePlanCount++];
+ entry.VisitIndex = visitIndex;
+ entry.CacheMapIndex = cacheMapIndex;
+ entry.IsFirst = isFirst;
+ entry.Value = value;
+ }
+
+ ///
+ /// Sorts write plan by VisitIndex for sequential cursor consumption in write pass.
+ /// Called once after scan pass completes.
+ ///
+ internal void SortWritePlan()
+ {
+ if (_writePlanCount > 1)
+ _writePlan.AsSpan(0, _writePlanCount).Sort(static (a, b) => a.VisitIndex.CompareTo(b.VisitIndex));
+ }
+
+ /// Write pass cursor index into sorted _writePlan array.
+ internal int WritePlanCursor;
+
+ /// Write pass visit counter. Mirrors ScanVisitIndex ordering.
+ internal int WriteVisitIndex;
+
+ ///
+ /// Tries to consume the next write plan entry at the current WriteVisitIndex.
+ /// Returns true if the entry matches (duplicate exists at this visit point).
+ /// Always increments WriteVisitIndex.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal bool TryConsumeWritePlanEntry(out WriteDuplicateEntry entry)
+ {
+ var visitIndex = WriteVisitIndex++;
+ if (_writePlan != null && WritePlanCursor < _writePlanCount)
+ {
+ ref var candidate = ref _writePlan[WritePlanCursor];
+ if (candidate.VisitIndex == visitIndex)
+ {
+ entry = candidate;
+ WritePlanCursor++;
+ return true;
+ }
+ }
+ entry = default;
+ return false;
+ }
+
+ #endregion
+
///
/// Next cache index reference for scan pass. Direct ref access for TryTrack methods.
///
@@ -182,6 +261,21 @@ public static partial class AcBinarySerializer
_stringInternMap?.Reset();
_nextCacheIndex = 0;
NextFirstIndex = 0;
+ ScanVisitIndex = 0;
+ WritePlanCursor = 0;
+ WriteVisitIndex = 0;
+
+ // Clear write plan string references to avoid GC pinning, keep array if small enough
+ if (_writePlan != null)
+ {
+ _writePlan.AsSpan(0, _writePlanCount).Clear();
+ if (_writePlan.Length > PropertyIndexBufferMaxCache)
+ {
+ ArrayPool.Shared.Return(_writePlan);
+ _writePlan = null;
+ }
+ }
+ _writePlanCount = 0;
if (_propertyIndexBuffer != null && _propertyIndexBuffer.Length > PropertyIndexBufferMaxCache)
{
@@ -590,10 +684,13 @@ public static partial class AcBinarySerializer
///
/// Scan pass: tracks a string for interning. Assigns CacheIndex immediately on 2nd occurrence.
+ /// Builds WriteDuplicateEntry for all duplicate occurrences.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void ScanInternString(string value)
{
+ var visitIndex = ScanVisitIndex++;
+
_stringInternMap ??= new IdentityMap();
if (!_stringInternMap.TryAdd(value, out var slotIndex))
@@ -602,15 +699,19 @@ public static partial class AcBinarySerializer
ref var entry = ref _stringInternMap.GetValueRef(slotIndex);
if (entry.CacheIndex == -1)
{
+ // 2nd occurrence: assign CacheIndex + add StringFirst entry at first visit position
entry.CacheIndex = ++_nextCacheIndex;
entry.IsFirstWrite = true;
+ AddWriteDuplicateEntry(entry.FirstIndex, entry.CacheIndex, isFirst: true, value);
}
+ // 2nd+ occurrence: add StringRef entry at current position
+ AddWriteDuplicateEntry(visitIndex, entry.CacheIndex, isFirst: false, value: null);
return;
}
- // 1st occurrence: store FirstIndex for validation, CacheIndex = -1 (not cached yet)
+ // 1st occurrence: store scan visit index, CacheIndex = -1 (not cached yet)
ref var newEntry = ref _stringInternMap.GetValueRef(slotIndex);
- newEntry.FirstIndex = NextFirstIndex++;
+ newEntry.FirstIndex = visitIndex;
newEntry.CacheIndex = -1;
}
diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.ScanPass.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.ScanPass.cs
index bda59f9..0298b9b 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.ScanPass.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.ScanPass.cs
@@ -24,7 +24,7 @@ public static partial class AcBinarySerializer
var wrapper = context.GetWrapper(type);
ScanValue(value, wrapper, context, 0);
- //context.CurrentProperty = null;
+ context.SortWritePlan();
}
private static void ScanValue(object? value, TypeMetadataWrapper wrapper, BinarySerializationContext context, int depth)
@@ -93,29 +93,46 @@ public static partial class AcBinarySerializer
// 3. Nested objects reachable from other paths are scanned through those paths
if (context.UseTypeReferenceHandling(metadata))
{
+ var visitIndex = context.ScanVisitIndex++;
+
// Direct tracking call - avoid extra indirection through context
bool isFirst;
+ int cacheIndex;
+ int firstVisitIndex;
switch (metadata.IdAccessorType)
{
case IdAccessorType.Int32:
var id32 = wrapper.RefIdGetterInt32!(value);
- isFirst = wrapper.TryTrackInt32(id32, context.NextFirstIndex++, ref context.NextCacheIndexRef, out _);
+ isFirst = wrapper.TryTrackInt32(id32, visitIndex, ref context.NextCacheIndexRef, out cacheIndex, out firstVisitIndex);
break;
case IdAccessorType.Int64:
var id64 = wrapper.RefIdGetterInt64!(value);
- isFirst = wrapper.TryTrackInt64(id64, context.NextFirstIndex++, ref context.NextCacheIndexRef, out _);
+ isFirst = wrapper.TryTrackInt64(id64, visitIndex, ref context.NextCacheIndexRef, out cacheIndex, out firstVisitIndex);
break;
case IdAccessorType.Guid:
var idGuid = wrapper.RefIdGetterGuid!(value);
- isFirst = wrapper.TryTrackGuid(idGuid, context.NextFirstIndex++, ref context.NextCacheIndexRef, out _);
+ isFirst = wrapper.TryTrackGuid(idGuid, visitIndex, ref context.NextCacheIndexRef, out cacheIndex, out firstVisitIndex);
break;
default:
isFirst = true;
+ cacheIndex = -1;
+ firstVisitIndex = -1;
break;
}
if (!isFirst)
+ {
+ // Build WriteDuplicateEntry for IId references
+ if (firstVisitIndex >= 0)
+ {
+ // Exact 2nd occurrence: add RefFirst entry at first visit position
+ context.AddWriteDuplicateEntry(firstVisitIndex, cacheIndex, isFirst: true, value: null);
+ }
+ // 2nd+ occurrence: add ObjRef at current position
+ context.AddWriteDuplicateEntry(visitIndex, cacheIndex, isFirst: false, value: null);
+
return; // 2nd occurrence → skip children (symmetric with write pass ObjectRef)
+ }
}
// Recursive scan on reference properties only
diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs
index fa94686..66e42e1 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs
@@ -4,6 +4,7 @@ using AyCode.Core.Serializers.Expressions;
using System.Buffers;
using System.Collections;
using System.Collections.Concurrent;
+using System.Diagnostics;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
@@ -899,7 +900,7 @@ public static partial class AcBinarySerializer
///
/// Optimized string writer with FixStr for short strings.
- /// Marker-based interning: write String marker, rewrite to StringInternFirst at end if needed.
+ /// Uses pre-computed WriteDuplicateEntry cursor for interning (no IdentityMap lookup).
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteString(string value, BinarySerializationContext context)
@@ -911,37 +912,29 @@ public static partial class AcBinarySerializer
return;
}
- if (context.UseStringInterning && context.IsValidForInterningString(value.Length))// && context.CurrentProperty!.UseStringPropertyInterning(context.Options.UseStringInterning))
+ if (context.UseStringInterning && context.IsValidForInterningString(value.Length))
{
- ref var interEntry = ref context.GetInternedStringEntry(value, out bool found);
-
- if (found && interEntry.CacheIndex >= 0)
+ if (context.TryConsumeWritePlanEntry(out var planEntry))
{
- if (interEntry.IsFirstWrite)
+ ValidateWritePlanString(in planEntry, value);
+ if (planEntry.IsFirst)
{
- // 1st serialize occurrence of a cached string - write StringInternFirst + cacheIndex + data
- interEntry.IsFirstWrite = false;
+ // StringFirst: write interned string + cache index + data (Value holds the string)
context.WriteByte(BinaryTypeCode.StringInternFirst);
- context.WriteVarUInt((uint)interEntry.CacheIndex);
- context.WriteStringUtf8(value);
+ context.WriteVarUInt((uint)planEntry.CacheMapIndex);
+ context.WriteStringUtf8(planEntry.Value ?? value);
}
else
{
- // 2+ serialize occurrence: write index reference
+ // StringRef: write index reference only (no getter call, no string data)
context.WriteByte(BinaryTypeCode.StringInterned);
- context.WriteVarUInt((uint)interEntry.CacheIndex);
+ context.WriteVarUInt((uint)planEntry.CacheMapIndex);
}
return;
}
- // CacheIndex < 0 or not found → single occurrence, fall through to FixStr/String path
+ // No plan entry → single occurrence, fall through to FixStr/String path
#if DEBUG
- //context.OnStringInterned?.Invoke(
- // context.CurrentProperty != null ? $"{context.CurrentProperty.DeclaringType.Name}.{context.CurrentProperty.Name}" : null,
- // value);
-
- context.OnStringInterned?.Invoke(
- null,
- value);
+ context.OnStringInterned?.Invoke(null, value);
#endif
}
@@ -991,86 +984,24 @@ public static partial class AcBinarySerializer
isFirstMetadataOccurrence = BinarySerializationContext.RegisterMetadataType(wrapper);
}
- // Reference handling: lookup entry from scan pass, check IsFirstWrite
+ // Reference handling: consume pre-computed write plan entry from scan pass cursor
var cachedObjectCacheIndex = -1; // -1 = not cached, 0+ = cache index for first write
if (context.UseTypeReferenceHandling(metadata))
{
- // Lookup by Id (IId types) or by object identity hash (non-IId types)
- // Both use IdAccessorType.Int32 - for non-IId, RefIdGetterInt32 returns RuntimeHelpers.GetHashCode
- switch (metadata.IdAccessorType)
+ if (context.TryConsumeWritePlanEntry(out var planEntry))
{
- case IdAccessorType.Int32:
+ ValidateWritePlanObject(in planEntry, value, wrapper);
+ if (planEntry.IsFirst)
{
- var id = wrapper.RefIdGetterInt32!(value);
- // For IId: skip default Id (0). For non-IId (hash): hash is never 0 for valid objects
- if ((!metadata.IsIId || id != 0) && wrapper.TryGetEntryInt32(id, out var slotIndex))
- {
- ref var entry = ref wrapper.GetEntryRefInt32(slotIndex);
- if (entry.CacheIndex >= 0)
- {
- if (entry.IsFirstWrite)
- {
- entry.IsFirstWrite = false;
- cachedObjectCacheIndex = entry.CacheIndex;
- }
- else
- {
- // 2+ occurrence → write ObjectRef
- context.WriteByte(BinaryTypeCode.ObjectRef);
- context.WriteVarUInt((uint)entry.CacheIndex);
- return;
- }
- }
- }
- break;
+ // First occurrence of a cached IId object — write full object + cache index
+ cachedObjectCacheIndex = planEntry.CacheMapIndex;
}
-
- case IdAccessorType.Int64:
+ else
{
- var id = wrapper.RefIdGetterInt64!(value);
- if (id != 0 && wrapper.TryGetEntryInt64(id, out var slotIndex))
- {
- ref var entry = ref wrapper.GetEntryRefInt64(slotIndex);
- if (entry.CacheIndex >= 0)
- {
- if (entry.IsFirstWrite)
- {
- entry.IsFirstWrite = false;
- cachedObjectCacheIndex = entry.CacheIndex;
- }
- else
- {
- context.WriteByte(BinaryTypeCode.ObjectRef);
- context.WriteVarUInt((uint)entry.CacheIndex);
- return;
- }
- }
- }
- break;
- }
-
- case IdAccessorType.Guid:
- {
- var id = wrapper.RefIdGetterGuid!(value);
- if (id != Guid.Empty && wrapper.TryGetEntryGuid(id, out var slotIndex))
- {
- ref var entry = ref wrapper.GetEntryRefGuid(slotIndex);
- if (entry.CacheIndex >= 0)
- {
- if (entry.IsFirstWrite)
- {
- entry.IsFirstWrite = false;
- cachedObjectCacheIndex = entry.CacheIndex;
- }
- else
- {
- context.WriteByte(BinaryTypeCode.ObjectRef);
- context.WriteVarUInt((uint)entry.CacheIndex);
- return;
- }
- }
- }
- break;
+ // 2+ occurrence → write ObjectRef (no children, no properties)
+ context.WriteByte(BinaryTypeCode.ObjectRef);
+ context.WriteVarUInt((uint)planEntry.CacheMapIndex);
+ return;
}
}
}
@@ -1781,4 +1712,85 @@ public static partial class AcBinarySerializer
// Type metadata helpers moved to AcBinarySerializer.BinarySerializeTypeMetadata.cs
#endregion
+
+ #region WritePlan Debug Validation
+
+ ///
+ /// DEBUG ONLY: Validates that the WritePlan entry's string value matches the actual string being written.
+ /// Catches scan/write pass visit index misalignment immediately.
+ ///
+ [Conditional("DEBUG")]
+ private static void ValidateWritePlanString(in WriteDuplicateEntry planEntry, string actualValue)
+ {
+ if (planEntry.IsFirst && planEntry.Value != null && !string.Equals(planEntry.Value, actualValue, StringComparison.Ordinal))
+ {
+ throw new InvalidOperationException(
+ $"WritePlan cursor mismatch at VisitIndex={planEntry.VisitIndex}: " +
+ $"plan string=\"{planEntry.Value}\", actual=\"{actualValue}\". " +
+ $"Scan/write pass visit order diverged.");
+ }
+ }
+
+ ///
+ /// DEBUG ONLY: Validates that the WritePlan entry's IId matches the actual object being written.
+ /// Uses the typed Id getter to extract the Id and compares against the scan pass IdentityMap.
+ ///
+ [Conditional("DEBUG")]
+ private static void ValidateWritePlanObject(
+ in WriteDuplicateEntry planEntry,
+ object value,
+ TypeMetadataWrapper wrapper)
+ {
+ var metadata = wrapper.Metadata;
+ switch (metadata.IdAccessorType)
+ {
+ case IdAccessorType.Int32:
+ {
+ var actualId = wrapper.RefIdGetterInt32!(value);
+ if (!wrapper.TryGetEntryInt32(actualId, out var slotIndex))
+ throw new InvalidOperationException(
+ $"WritePlan cursor mismatch at VisitIndex={planEntry.VisitIndex}: " +
+ $"Int32 Id={actualId} not found in IdentityMap. Scan/write pass visit order diverged.");
+
+ ref var entry = ref wrapper.GetEntryRefInt32(slotIndex);
+ if (entry.CacheIndex != planEntry.CacheMapIndex)
+ throw new InvalidOperationException(
+ $"WritePlan cursor mismatch at VisitIndex={planEntry.VisitIndex}: " +
+ $"Int32 Id={actualId} CacheIndex={entry.CacheIndex}, plan CacheMapIndex={planEntry.CacheMapIndex}.");
+ break;
+ }
+ case IdAccessorType.Int64:
+ {
+ var actualId = wrapper.RefIdGetterInt64!(value);
+ if (!wrapper.TryGetEntryInt64(actualId, out var slotIndex))
+ throw new InvalidOperationException(
+ $"WritePlan cursor mismatch at VisitIndex={planEntry.VisitIndex}: " +
+ $"Int64 Id={actualId} not found in IdentityMap. Scan/write pass visit order diverged.");
+
+ ref var entry = ref wrapper.GetEntryRefInt64(slotIndex);
+ if (entry.CacheIndex != planEntry.CacheMapIndex)
+ throw new InvalidOperationException(
+ $"WritePlan cursor mismatch at VisitIndex={planEntry.VisitIndex}: " +
+ $"Int64 Id={actualId} CacheIndex={entry.CacheIndex}, plan CacheMapIndex={planEntry.CacheMapIndex}.");
+ break;
+ }
+ case IdAccessorType.Guid:
+ {
+ var actualId = wrapper.RefIdGetterGuid!(value);
+ if (!wrapper.TryGetEntryGuid(actualId, out var slotIndex))
+ throw new InvalidOperationException(
+ $"WritePlan cursor mismatch at VisitIndex={planEntry.VisitIndex}: " +
+ $"Guid Id={actualId} not found in IdentityMap. Scan/write pass visit order diverged.");
+
+ ref var entry = ref wrapper.GetEntryRefGuid(slotIndex);
+ if (entry.CacheIndex != planEntry.CacheMapIndex)
+ throw new InvalidOperationException(
+ $"WritePlan cursor mismatch at VisitIndex={planEntry.VisitIndex}: " +
+ $"Guid Id={actualId} CacheIndex={entry.CacheIndex}, plan CacheMapIndex={planEntry.CacheMapIndex}.");
+ break;
+ }
+ }
+ }
+
+ #endregion
}
diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs
index a41a6df..bbc3c38 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs
@@ -84,7 +84,7 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
///
public bool UseMetadata { get; set; } = false;
- public bool UseGeneratedCode { get; set; } = true;
+ public bool UseGeneratedCode { get; set; } = false;
///
/// When true, checks for duplicate property name hashes during serialization (UseMetadata mode).
diff --git a/AyCode.Core/Serializers/IdentityMap.cs b/AyCode.Core/Serializers/IdentityMap.cs
index 8a9ded6..b19616e 100644
--- a/AyCode.Core/Serializers/IdentityMap.cs
+++ b/AyCode.Core/Serializers/IdentityMap.cs
@@ -28,7 +28,7 @@ public enum IdAccessorType : byte
///
public struct InternEntry
{
- /// Order of first occurrence during scan pass (0, 1, 2, ...). Used for CacheIndex assignment.
+ /// Scan visit index of first occurrence. Used to create WriteDuplicateEntry on 2nd occurrence.
public int FirstIndex;
/// Dense cache index (0, 1, 2, ...) assigned after scan pass. -1 = not cached, 0+ = cache index.
public int CacheIndex;
@@ -36,6 +36,23 @@ public struct InternEntry
public bool IsFirstWrite;
}
+///
+/// Pre-computed write instruction for duplicate strings and IId object references.
+/// Built during scan pass, sorted by VisitIndex, consumed sequentially by write pass cursor.
+/// Eliminates IdentityMap lookups and redundant getter calls from the write hot path.
+///
+public struct WriteDuplicateEntry
+{
+ /// Sequential visit index matching the write pass traversal order.
+ public int VisitIndex;
+ /// Cache/intern index to write (intern index for strings, cache index for IId objects).
+ public int CacheMapIndex;
+ /// True = first occurrence (StringFirst / RefFirst). False = subsequent reference (StringRef / ObjRef).
+ public bool IsFirst;
+ /// Non-null for StringFirst: the interned string value (avoids getter call). Null for all other cases.
+ public string? Value;
+}
+
///
/// Interface for identity maps used in serialization tracking.
/// Enables type-safe Reset() without knowing the generic type parameter.
diff --git a/AyCode.Core/Serializers/TypeMetadataWrapper.cs b/AyCode.Core/Serializers/TypeMetadataWrapper.cs
index f654c4c..d2c6de6 100644
--- a/AyCode.Core/Serializers/TypeMetadataWrapper.cs
+++ b/AyCode.Core/Serializers/TypeMetadataWrapper.cs
@@ -209,11 +209,12 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat
///
/// Tries to track Int32 Id. Returns true if first occurrence.
/// On 2+ occurrence: assigns CacheIndex immediately using ++nextCacheIndex.
+ /// firstVisitIndex >= 0 only on exact 2nd occurrence (CacheIndex transition from -1).
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public bool TryTrackInt32(int id, int firstIndex, ref int nextCacheIndex, out int cacheIndex)
+ public bool TryTrackInt32(int id, int visitIndex, ref int nextCacheIndex, out int cacheIndex, out int firstVisitIndex)
{
- if (id == 0) { cacheIndex = -1; return true; } // Default Id - no tracking
+ if (id == 0) { cacheIndex = -1; firstVisitIndex = -1; return true; } // Default Id - no tracking
var map = IdentityMapInt32 ??= new IdentityMap();
if (!map.TryAdd(id, out var slotIndex))
@@ -222,28 +223,36 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat
ref var entry = ref map.GetValueRef(slotIndex);
if (entry.CacheIndex == -1)
{
+ // Exact 2nd occurrence: assign CacheIndex + return first visit index
entry.CacheIndex = ++nextCacheIndex;
entry.IsFirstWrite = true;
+ firstVisitIndex = entry.FirstIndex;
+ }
+ else
+ {
+ firstVisitIndex = -1; // 3rd+ occurrence
}
cacheIndex = entry.CacheIndex;
return false;
}
- // 1st occurrence: store FirstIndex for validation
+ // 1st occurrence: store scan visit index
ref var newEntry = ref map.GetValueRef(slotIndex);
- newEntry.FirstIndex = firstIndex;
+ newEntry.FirstIndex = visitIndex;
newEntry.CacheIndex = -1;
cacheIndex = -1;
+ firstVisitIndex = -1;
return true;
}
///
/// Tries to track Int64 Id. Returns true if first occurrence.
+ /// firstVisitIndex >= 0 only on exact 2nd occurrence.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public bool TryTrackInt64(long id, int firstIndex, ref int nextCacheIndex, out int cacheIndex)
+ public bool TryTrackInt64(long id, int visitIndex, ref int nextCacheIndex, out int cacheIndex, out int firstVisitIndex)
{
- if (id == 0) { cacheIndex = -1; return true; }
+ if (id == 0) { cacheIndex = -1; firstVisitIndex = -1; return true; }
var map = IdentityMapInt64 ??= new IdentityMap();
if (!map.TryAdd(id, out var slotIndex))
@@ -253,25 +262,32 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat
{
entry.CacheIndex = ++nextCacheIndex;
entry.IsFirstWrite = true;
+ firstVisitIndex = entry.FirstIndex;
+ }
+ else
+ {
+ firstVisitIndex = -1;
}
cacheIndex = entry.CacheIndex;
return false;
}
ref var newEntry = ref map.GetValueRef(slotIndex);
- newEntry.FirstIndex = firstIndex;
+ newEntry.FirstIndex = visitIndex;
newEntry.CacheIndex = -1;
cacheIndex = -1;
+ firstVisitIndex = -1;
return true;
}
///
/// Tries to track Guid Id. Returns true if first occurrence.
+ /// firstVisitIndex >= 0 only on exact 2nd occurrence.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public bool TryTrackGuid(Guid id, int firstIndex, ref int nextCacheIndex, out int cacheIndex)
+ public bool TryTrackGuid(Guid id, int visitIndex, ref int nextCacheIndex, out int cacheIndex, out int firstVisitIndex)
{
- if (id == Guid.Empty) { cacheIndex = -1; return true; }
+ if (id == Guid.Empty) { cacheIndex = -1; firstVisitIndex = -1; return true; }
var map = IdentityMapGuid ??= new IdentityMap();
if (!map.TryAdd(id, out var slotIndex))
@@ -281,15 +297,21 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat
{
entry.CacheIndex = ++nextCacheIndex;
entry.IsFirstWrite = true;
+ firstVisitIndex = entry.FirstIndex;
+ }
+ else
+ {
+ firstVisitIndex = -1;
}
cacheIndex = entry.CacheIndex;
return false;
}
ref var newEntry = ref map.GetValueRef(slotIndex);
- newEntry.FirstIndex = firstIndex;
+ newEntry.FirstIndex = visitIndex;
newEntry.CacheIndex = -1;
cacheIndex = -1;
+ firstVisitIndex = -1;
return true;
}