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);
}
}