Optimize AcBinary scan, string caching, and benchmarks
- Refactor collection scan to pre-cache element wrappers and optimize ScanItem for polymorphic types - Add DisableStringCaching to deserializer; call it on first interned string marker - Update benchmarks to restore default and no-ref variants, clarify string interning options - Ensure property scanning respects property filters, skipping filtered properties
This commit is contained in:
parent
f84dcb773d
commit
a0a6ac8ef4
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -276,6 +276,16 @@ public static partial class AcBinaryDeserializer
|
|||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Called on first StringInternFirst marker — disables _stringCache because
|
||||
/// interned strings are resolved via _internCache and plain strings appear only once.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal void DisableStringCaching()
|
||||
{
|
||||
_useStringCaching = false;
|
||||
}
|
||||
|
||||
#region Bytes & String Reading
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
|
|
|
|||
|
|
@ -912,6 +912,9 @@ public static partial class AcBinaryDeserializer
|
|||
private static string ReadAndRegisterInternedString<TInput>(BinaryDeserializationContext<TInput> 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;
|
||||
|
|
|
|||
|
|
@ -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
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void ScanItem<TOutput>(object item, BinarySerializationContext<TOutput> context, int depth)
|
||||
private static void ScanItem<TOutput>(object item, TypeMetadataWrapper<BinarySerializeTypeMetadata>? elementWrapper, BinarySerializationContext<TOutput> 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue