diff --git a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs index 81544b1..54d42f1 100644 --- a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs +++ b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs @@ -311,9 +311,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator sb.AppendLine($"internal sealed class {ci.ClassName}_GeneratedWriter : IGeneratedBinaryWriter"); sb.AppendLine("{"); sb.AppendLine($" internal static readonly {ci.ClassName}_GeneratedWriter Instance = new();"); - sb.AppendLine($" internal static readonly int s_metadataSlot = AcBinarySerializer.AllocateMetadataSlot();"); - if (ci.IsIId || ci.EnableRefHandling) - sb.AppendLine($" internal static readonly int s_trackingSlot = AcBinarySerializer.AllocateTrackingSlot();"); + sb.AppendLine($" internal static readonly int s_wrapperSlot = AcBinarySerializer.AllocateWrapperSlot();"); sb.AppendLine($" internal static readonly int s_typeNameHash = {ci.TypeNameHash};"); sb.Append( $" internal static readonly int[] s_propertyHashes = new int[] {{ "); sb.Append(string.Join(", ", ci.PropertyNameHashes)); @@ -384,33 +382,46 @@ public class AcBinarySourceGenerator : IIncrementalGenerator sb.AppendLine(); sb.AppendLine($" var obj = Unsafe.As<{ci.FullTypeName}>(value);"); - // Self ref tracking — matches runtime ScanValue UseTypeReferenceHandling block + // Self ref tracking — inline TryTrack via wrapper (no bridge method overhead) // Only emitted when the corresponding feature flag is enabled. if (ci.IsIId) { - // IId type: track when ReferenceHandling != None - var trackMethod = ci.IdTypeName switch + var tryTrackMethod = ci.IdTypeName switch { - "int" => "ScanTrackObjectInt32", - "long" => "ScanTrackObjectInt64", - "System.Guid" => "ScanTrackObjectGuid", - _ => "ScanTrackObjectInt32" + "int" => "TryTrackInt32", + "long" => "TryTrackInt64", + "System.Guid" => "TryTrackGuid", + _ => "TryTrackInt32" }; sb.AppendLine(); sb.AppendLine(" if (context.ReferenceHandling != ReferenceHandlingMode.None)"); sb.AppendLine(" {"); - sb.AppendLine($" if (!context.{trackMethod}(s_trackingSlot, obj.Id))"); + sb.AppendLine($" var wrapper = context.GetWrapperBySlot(s_wrapperSlot, typeof({ci.FullTypeName}));"); + sb.AppendLine(" var visitIndex = context.ScanVisitIndex++;"); + sb.AppendLine($" if (!wrapper.{tryTrackMethod}(obj.Id, visitIndex, ref context.NextCacheIndexRef, out var cacheIndex, out var firstVisitIndex))"); + sb.AppendLine(" {"); + sb.AppendLine(" if (firstVisitIndex >= 0)"); + sb.AppendLine(" context.AddWriteDuplicateEntry(firstVisitIndex, cacheIndex, isFirst: true, value: null);"); + sb.AppendLine(" context.AddWriteDuplicateEntry(visitIndex, cacheIndex, isFirst: false, value: null);"); sb.AppendLine(" return;"); + sb.AppendLine(" }"); sb.AppendLine(" }"); } else if (ci.EnableRefHandling) { - // Non-IId type: track when ReferenceHandling == All + // Non-IId type: track via wrapper.TryTrackInt32 with RuntimeHelpers.GetHashCode sb.AppendLine(); sb.AppendLine(" if (context.ReferenceHandling == ReferenceHandlingMode.All)"); sb.AppendLine(" {"); - sb.AppendLine(" if (!context.ScanTrackObjectInt32(s_trackingSlot, System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(value)))"); + sb.AppendLine($" var wrapper = context.GetWrapperBySlot(s_wrapperSlot, typeof({ci.FullTypeName}));"); + sb.AppendLine(" var visitIndex = context.ScanVisitIndex++;"); + sb.AppendLine(" if (!wrapper.TryTrackInt32(System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(value), visitIndex, ref context.NextCacheIndexRef, out var cacheIndex, out var firstVisitIndex))"); + sb.AppendLine(" {"); + sb.AppendLine(" if (firstVisitIndex >= 0)"); + sb.AppendLine(" context.AddWriteDuplicateEntry(firstVisitIndex, cacheIndex, isFirst: true, value: null);"); + sb.AppendLine(" context.AddWriteDuplicateEntry(visitIndex, cacheIndex, isFirst: false, value: null);"); sb.AppendLine(" return;"); + sb.AppendLine(" }"); sb.AppendLine(" }"); } @@ -891,7 +902,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator else { // UseMetadata: register type for first/repeated tracking via slot (no GetWrapper needed) - sb.AppendLine($"{i} var isFirstMeta_{p.Name} = context.UseMetadata && context.RegisterMetadataTypeBySlot({writer}.s_metadataSlot);"); + sb.AppendLine($"{i} var isFirstMeta_{p.Name} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext.RegisterMetadataType(context.GetWrapperBySlot({writer}.s_wrapperSlot, typeof({p.TypeNameForTypeof})));"); // Inline ref tracking: guard depends on IId vs non-IId to match scan pass behavior. var refGuard = p.IsIId @@ -1041,7 +1052,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator else { // UseMetadata: register element type for first/repeated tracking - sb.AppendLine($"{i} var isFirstMeta_e_{p.Name} = context.UseMetadata && context.RegisterMetadataTypeBySlot({writer}.s_metadataSlot);"); + sb.AppendLine($"{i} var isFirstMeta_e_{p.Name} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext.RegisterMetadataType(context.GetWrapperBySlot({writer}.s_wrapperSlot, typeof({p.ElementFullTypeName})));"); sb.AppendLine($"{i} if ({elemRefGuard} && context.TryConsumeWritePlanEntry(out var epe_{p.Name}))"); sb.AppendLine($"{i} {{"); diff --git a/AyCode.Core/Serializers/AcSerializerContextBase.cs b/AyCode.Core/Serializers/AcSerializerContextBase.cs index ac16d16..ced1287 100644 --- a/AyCode.Core/Serializers/AcSerializerContextBase.cs +++ b/AyCode.Core/Serializers/AcSerializerContextBase.cs @@ -36,6 +36,13 @@ public abstract class AcSerializerContextBase /// private readonly Dictionary> _wrappers = new(); + /// + /// Slot-indexed wrapper cache for SGen types. Indexed by AllocateWrapperSlot() value. + /// Avoids dictionary lookup — direct array access for types with compile-time known slot index. + /// Not cleared on pool return: wrapper references are stable across serialization calls. + /// + private TypeMetadataWrapper?[]? _wrapperSlots; + /// /// Factory function to create metadata. Implemented by derived class. /// @@ -81,6 +88,44 @@ public abstract class AcSerializerContextBase return wrapper; } + /// + /// Gets or creates a wrapper for the specified type using a pre-allocated slot index. + /// SGen types call this with their compile-time known slot — avoids dictionary lookup. + /// First call per slot per context: falls back to GetWrapper + stores in slot array. + /// Subsequent calls: direct array index (~1-2ns vs ~15-25ns dictionary lookup). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TypeMetadataWrapper GetWrapperBySlot(int slot, Type type) + { + var slots = _wrapperSlots; + if (slots != null && (uint)slot < (uint)slots.Length) + { + var wrapper = slots[slot]; + if (wrapper != null) + return wrapper; + } + + return GetWrapperBySlotSlow(slot, type); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private TypeMetadataWrapper GetWrapperBySlotSlow(int slot, Type type) + { + var wrapper = GetWrapper(type); + _wrapperSlots![slot] = wrapper; + return wrapper; + } + + /// + /// Pre-allocates the wrapper slot array with the known total slot count. + /// Called once from derived context constructor after all AllocateWrapperSlot() calls have completed. + /// + protected void InitializeWrapperSlots(int count) + { + if (count > 0) + _wrapperSlots = new TypeMetadataWrapper?[count]; + } + #endregion #region Wrapper Iteration (for footer writing) diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs index cacd312..c36ca8f 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs @@ -89,26 +89,16 @@ public static partial class AcBinarySerializer #endregion private IdentityMap? _stringInternMap; - private readonly ulong[] _metadataSeenBits = new ulong[4]; // 256 SGen slot (bit per type, first/repeated tracking) - private HashSet? _metadataSeenOverflow; // fallback for slot >= 256 - private int _nextCacheIndex; // Next dense cache index to assign (starts at 0, uses ++_nextCacheIndex) + private int _nextCacheIndex; public int NextFirstIndex; // Next first occurrence index for scan pass. Direct access for performance. - #region Slot-based IdentityMaps for SGen scan pass (replaces wrapper-based tracking) - - private IdentityMap?[]? _slottedIdMapsInt32; - private IdentityMap?[]? _slottedIdMapsInt64; - private IdentityMap?[]? _slottedIdMapsGuid; - - #endregion - #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; + public int ScanVisitIndex; /// Write plan entry count for write pass cursor. internal int WritePlanCount => _writePlanCount; @@ -119,7 +109,7 @@ public static partial class AcBinarySerializer /// /// 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) + public void AddWriteDuplicateEntry(int visitIndex, int cacheMapIndex, bool isFirst, string? value) { if (_writePlan == null) { @@ -261,7 +251,7 @@ public static partial class AcBinarySerializer public BinarySerializationContext(AcBinarySerializerOptions options) { Reset(options); - //FastWire = options.WireMode == WireMode.Fast; + InitializeWrapperSlots(Volatile.Read(ref s_nextWrapperSlot)); } /// @@ -287,12 +277,6 @@ public static partial class AcBinarySerializer } _stringInternMap?.Reset(); - _metadataSeenBits.AsSpan().Clear(); - _metadataSeenOverflow?.Clear(); - - ResetSlottedMaps(_slottedIdMapsInt32); - ResetSlottedMaps(_slottedIdMapsInt64); - ResetSlottedMaps(_slottedIdMapsGuid); _nextCacheIndex = 0; NextFirstIndex = 0; @@ -782,175 +766,22 @@ public static partial class AcBinarySerializer #endregion - #region Slot-based Scan Pass Ref Tracking (SGen) - - /// - /// SGen scan pass: tracks an object by Int32 Id (IId or RuntimeHelpers.GetHashCode for non-IId). - /// Increments ScanVisitIndex, manages IdentityMap via slot, and builds WriteDuplicateEntry on duplicates. - /// Returns true if first occurrence (caller should scan children), false if duplicate (skip children). - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool ScanTrackObjectInt32(int slot, int id) - { - var visitIndex = ScanVisitIndex++; - - if (id == 0) return true; // default Id — no tracking - - ref var maps = ref _slottedIdMapsInt32; - if (maps == null || maps.Length <= slot) - GrowSlottedMaps(ref maps, slot); - - var map = maps[slot] ??= new IdentityMap(); - - if (!map.TryAdd(id, out var si)) - { - ref var entry = ref map.GetValueRef(si); - if (entry.CacheIndex == -1) - { - entry.CacheIndex = ++_nextCacheIndex; - entry.IsFirstWrite = true; - AddWriteDuplicateEntry(entry.FirstIndex, entry.CacheIndex, isFirst: true, value: null); - } - AddWriteDuplicateEntry(visitIndex, entry.CacheIndex, isFirst: false, value: null); - return false; - } - - ref var ne = ref map.GetValueRef(si); - ne.FirstIndex = visitIndex; - ne.CacheIndex = -1; - return true; - } - - /// - /// SGen scan pass: tracks an object by Int64 Id. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool ScanTrackObjectInt64(int slot, long id) - { - var visitIndex = ScanVisitIndex++; - - if (id == 0) return true; - - ref var maps = ref _slottedIdMapsInt64; - if (maps == null || maps.Length <= slot) - GrowSlottedMaps(ref maps, slot); - - var map = maps[slot] ??= new IdentityMap(); - - if (!map.TryAdd(id, out var si)) - { - ref var entry = ref map.GetValueRef(si); - if (entry.CacheIndex == -1) - { - entry.CacheIndex = ++_nextCacheIndex; - entry.IsFirstWrite = true; - AddWriteDuplicateEntry(entry.FirstIndex, entry.CacheIndex, isFirst: true, value: null); - } - AddWriteDuplicateEntry(visitIndex, entry.CacheIndex, isFirst: false, value: null); - return false; - } - - ref var ne = ref map.GetValueRef(si); - ne.FirstIndex = visitIndex; - ne.CacheIndex = -1; - return true; - } - - /// - /// SGen scan pass: tracks an object by Guid Id. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool ScanTrackObjectGuid(int slot, Guid id) - { - var visitIndex = ScanVisitIndex++; - - if (id == Guid.Empty) return true; - - ref var maps = ref _slottedIdMapsGuid; - if (maps == null || maps.Length <= slot) - GrowSlottedMaps(ref maps, slot); - - var map = maps[slot] ??= new IdentityMap(); - - if (!map.TryAdd(id, out var si)) - { - ref var entry = ref map.GetValueRef(si); - if (entry.CacheIndex == -1) - { - entry.CacheIndex = ++_nextCacheIndex; - entry.IsFirstWrite = true; - AddWriteDuplicateEntry(entry.FirstIndex, entry.CacheIndex, isFirst: true, value: null); - } - AddWriteDuplicateEntry(visitIndex, entry.CacheIndex, isFirst: false, value: null); - return false; - } - - ref var ne = ref map.GetValueRef(si); - ne.FirstIndex = visitIndex; - ne.CacheIndex = -1; - return true; - } - - [MethodImpl(MethodImplOptions.NoInlining)] - private static void GrowSlottedMaps(ref TMap?[]? maps, int slot) where TMap : class - { - var newSize = Math.Max(slot + 1, 16); - if (maps == null) - { - maps = new TMap?[newSize]; - return; - } - var newMaps = new TMap?[newSize]; - maps.AsSpan().CopyTo(newMaps); - maps = newMaps; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void ResetSlottedMaps(IdentityMap?[]? maps) - where TKey : notnull where TValue : struct - { - if (maps == null) return; - for (var i = 0; i < maps.Length; i++) - maps[i]?.Reset(); - } - - #endregion - #region UseMetadata Type Tracking /// - /// Regisztrálja a típust UseMetadata módban. - /// Visszaadja true-t ha ez az első előfordulás (inline hash-eket kell írni), - /// false-t ha ismételt (csak propNameHash kell). + /// Registers a type for UseMetadata first/repeated tracking. + /// Returns true on first occurrence (caller should write inline property hashes), + /// false on repeated (caller writes only typeNameHash). + /// Used by both runtime and SGen paths via wrapper. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool RegisterMetadataType(TypeMetadataWrapper wrapper) { - if (wrapper.MetadataFooterIndex >= 0) - return false; // ismételt + if (wrapper.MetadataSeen) + return false; - wrapper.MetadataFooterIndex = 0; // jelöljük hogy már regisztrálva - return true; // első előfordulás - } - - /// - /// SGen inline metadata regisztráció — slot-alapú, nem kell TypeMetadataWrapper. - /// Minden SGen típus kap egy compile-time slot indexet (AllocateMetadataSlot). - /// Első 256 slot: bit művelet (ulong[4]), felette: HashSet fallback. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool RegisterMetadataTypeBySlot(int slot) - { - if (slot < 256) - { - ref var bits = ref _metadataSeenBits[slot >> 6]; - var mask = 1UL << (slot & 63); - if ((bits & mask) != 0) return false; - bits |= mask; - return true; - } - _metadataSeenOverflow ??= new HashSet(); - return _metadataSeenOverflow.Add(slot); + wrapper.MetadataSeen = true; + return true; } /// diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs index 0aaf039..d600a2f 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs @@ -251,29 +251,16 @@ public static partial class AcBinarySerializer GeneratedWriterRegistry.Register(type, writer); } - #region UseMetadata Slot Allocation + #region SGen Slot Allocation - private static int s_nextMetadataSlot; + private static int s_nextWrapperSlot; /// - /// Allocates a unique slot index for SGen UseMetadata first/repeated tracking. - /// Called once per SGen type at startup (ModuleInitializer). Thread-safe. - /// Slot is used by BinarySerializationContext.RegisterMetadataTypeBySlot. + /// Allocates a unique slot index for SGen wrapper cache. + /// Indexes _wrapperSlots array on AcSerializerContextBase. + /// Used for: IdentityMap ref tracking (scan pass), MetadataSeen (write pass). /// - internal static int AllocateMetadataSlot() => Interlocked.Increment(ref s_nextMetadataSlot) - 1; - - #endregion - - #region Scan Pass Tracking Slot Allocation - - private static int s_nextTrackingSlot; - - /// - /// Allocates a unique slot index for SGen scan pass ref tracking IdentityMaps. - /// Called once per SGen type at startup (ModuleInitializer). Thread-safe. - /// Slot indexes the per-context IdentityMap arrays in BinarySerializationContext. - /// - internal static int AllocateTrackingSlot() => Interlocked.Increment(ref s_nextTrackingSlot) - 1; + internal static int AllocateWrapperSlot() => Interlocked.Increment(ref s_nextWrapperSlot) - 1; #endregion @@ -553,6 +540,25 @@ public static partial class AcBinarySerializer WriteObject(value, wrapper, context, depth, isNested: depth > 0); } + /// + /// Bridge for generated writers: writes a non-null complex OBJECT using slot-based wrapper lookup. + /// SGen types pass their compile-time known slot index — avoids GetWrapper dictionary lookup. + /// First call per slot per context: populates slot from GetWrapper. Subsequent calls: direct array index. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void WriteObjectGenerated(object value, Type type, int wrapperSlot, BinarySerializationContext context, int depth) + where TOutput : struct, IBinaryOutputBase + { + if (depth > context.MaxDepth) + { + context.WriteByte(BinaryTypeCode.Null); + return; + } + + var wrapper = context.GetWrapperBySlot(wrapperSlot, type); + WriteObject(value, wrapper, context, depth, isNested: depth > 0); + } + #endregion #region Value Writing diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs index e0fff73..6b01f75 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs @@ -82,7 +82,7 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions /// allowing the deserializer to match properties by name between different types. /// Default: false (no overhead) /// - public bool UseMetadata { get; set; } = true; + public bool UseMetadata { get; set; } = false; public bool UseGeneratedCode { get; set; } = true; diff --git a/AyCode.Core/Serializers/TypeMetadataWrapper.cs b/AyCode.Core/Serializers/TypeMetadataWrapper.cs index d2c6de6..597f81d 100644 --- a/AyCode.Core/Serializers/TypeMetadataWrapper.cs +++ b/AyCode.Core/Serializers/TypeMetadataWrapper.cs @@ -37,11 +37,11 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat internal readonly Func? RefIdGetterGuid; /// - /// UseMetadata: footer entry index for this type in the current serialization session. - /// -1 = not yet registered. Set by RegisterMetadataType, reset by ResetTracking. - /// Eliminates the need for Dictionary<Type, int> lookup in the serializer hot path. + /// UseMetadata: has this type been seen in the current serialization session? + /// false = not yet registered. Set by RegisterMetadataType, reset by ResetTracking. + /// Used by both runtime and SGen paths (single wrapper per type per context). /// - internal int MetadataFooterIndex = -1; + internal bool MetadataSeen; /// /// UseMetadata cachemap: source property index → target PropertySetter. @@ -190,7 +190,7 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat [MethodImpl(MethodImplOptions.AggressiveInlining)] public void ResetTracking(bool preRentBuckets = false) { - MetadataFooterIndex = -1; + MetadataSeen = false; CacheMap = null; // Options may change between sessions (pool reuse) → rebuild on next scan