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/AcBinaryDeserializer.BinaryDeserializationContext.Read.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.Read.cs index b1c7e00..69313b9 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.Read.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.Read.cs @@ -276,6 +276,16 @@ public static partial class AcBinaryDeserializer #endregion + /// + /// Called on first StringInternFirst marker — disables _stringCache because + /// interned strings are resolved via _internCache and plain strings appear only once. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void DisableStringCaching() + { + _useStringCaching = false; + } + #region Bytes & String Reading [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs index 8e8645b..75f68d8 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs @@ -912,6 +912,9 @@ public static partial class AcBinaryDeserializer private static string ReadAndRegisterInternedString(BinaryDeserializationContext context) where TInput : struct, IBinaryInputBase { + // First StringInternFirst marker proves payload uses string interning → + // plain String entries appear only once, so _stringCache would never hit + context.DisableStringCaching(); var cacheIndex = (int)context.ReadVarUInt(); var length = (int)context.ReadVarUInt(); if (length == 0) return string.Empty; diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.ScanPass.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.ScanPass.cs index b12b69c..394d10c 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.ScanPass.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.ScanPass.cs @@ -40,16 +40,22 @@ public static partial class AcBinarySerializer // Collection → iterate elements using IList fast path (no IEnumerator alloc) if (metadata.IsCollection) { - if (metadata.ElementNeedsScan) + var isStringCollectionElementType = false; + + if (metadata.ElementNeedsScan && + !((isStringCollectionElementType = ReferenceEquals(metadata.CollectionElementType, StringType)) && !context.UseStringInterning)) { var nextDepth = depth + 1; + // Pre-cache element wrapper for non-string collections (string fast path in ScanItem doesn't need it) + var elementWrapper = isStringCollectionElementType ? null : context.GetWrapper(metadata.CollectionElementType!); + if (value is IList list) { for (var i = 0; i < list.Count; i++) { var item = list[i]; if (item != null) - ScanItem(item, context, nextDepth); + ScanItem(item, elementWrapper, context, nextDepth); } } else if (value is IEnumerable enumerable) @@ -57,7 +63,7 @@ public static partial class AcBinarySerializer foreach (var item in enumerable) { if (item != null) - ScanItem(item, context, nextDepth); + ScanItem(item, elementWrapper, context, nextDepth); } } } @@ -102,10 +108,16 @@ public static partial class AcBinarySerializer // Recursive scan on reference properties only // Use typed getter for strings (much faster than reflection GetValue) var refProperties = metadata.ReferenceProperties; + var hasPropertyFilter = context.HasPropertyFilter; var nextDepth2 = depth + 1; for (var i = 0; i < refProperties.Length; i++) { var prop = refProperties[i]; + // Must match write pass: filtered properties write PropertySkip (no value) → + // scanning them would assign CacheIndex for strings/objects never in output + if (hasPropertyFilter && !context.ShouldSerializeProperty(value, prop)) + continue; + if (prop.AccessorType == PropertyAccessorType.String) { // Fast path: typed getter for string @@ -127,10 +139,11 @@ public static partial class AcBinarySerializer } /// - /// Scans a collection item. Handles string fast path and gets wrapper for the runtime type. + /// Scans a collection item. Uses pre-cached element wrapper when runtime type matches, + /// falls back to GetWrapper for polymorphic items. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void ScanItem(object item, BinarySerializationContext context, int depth) + private static void ScanItem(object item, TypeMetadataWrapper? elementWrapper, BinarySerializationContext context, int depth) where TOutput : struct, IBinaryOutputBase { // String fast path — avoid GetWrapper entirely @@ -141,7 +154,7 @@ public static partial class AcBinarySerializer return; } - var itemWrapper = context.GetWrapper(item.GetType()); + var itemWrapper = item.GetType() == elementWrapper!.Metadata.SourceType ? elementWrapper : context.GetWrapper(item.GetType()); ScanValue(item, itemWrapper, context, depth); } }