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
|
// AcBinary variants
|
||||||
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.Default, SerializerAcBinaryDefault),
|
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.Default, SerializerAcBinaryDefault),
|
||||||
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.WithoutReferenceHandling, SerializerAcBinaryNoRef),
|
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.FastMode, SerializerAcBinaryFastMode),
|
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
|
// AcJson
|
||||||
new AcJsonBenchmark(testData.Order, AcJsonSerializerOptions.Default, SerializerAcJsonDefault),
|
new AcJsonBenchmark(testData.Order, AcJsonSerializerOptions.Default, SerializerAcJsonDefault),
|
||||||
|
|
|
||||||
|
|
@ -276,6 +276,16 @@ public static partial class AcBinaryDeserializer
|
||||||
|
|
||||||
#endregion
|
#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
|
#region Bytes & String Reading
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
|
|
||||||
|
|
@ -912,6 +912,9 @@ public static partial class AcBinaryDeserializer
|
||||||
private static string ReadAndRegisterInternedString<TInput>(BinaryDeserializationContext<TInput> context)
|
private static string ReadAndRegisterInternedString<TInput>(BinaryDeserializationContext<TInput> context)
|
||||||
where TInput : struct, IBinaryInputBase
|
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 cacheIndex = (int)context.ReadVarUInt();
|
||||||
var length = (int)context.ReadVarUInt();
|
var length = (int)context.ReadVarUInt();
|
||||||
if (length == 0) return string.Empty;
|
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)
|
// Collection → iterate elements using IList fast path (no IEnumerator alloc)
|
||||||
if (metadata.IsCollection)
|
if (metadata.IsCollection)
|
||||||
{
|
{
|
||||||
if (metadata.ElementNeedsScan)
|
var isStringCollectionElementType = false;
|
||||||
|
|
||||||
|
if (metadata.ElementNeedsScan &&
|
||||||
|
!((isStringCollectionElementType = ReferenceEquals(metadata.CollectionElementType, StringType)) && !context.UseStringInterning))
|
||||||
{
|
{
|
||||||
var nextDepth = depth + 1;
|
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)
|
if (value is IList list)
|
||||||
{
|
{
|
||||||
for (var i = 0; i < list.Count; i++)
|
for (var i = 0; i < list.Count; i++)
|
||||||
{
|
{
|
||||||
var item = list[i];
|
var item = list[i];
|
||||||
if (item != null)
|
if (item != null)
|
||||||
ScanItem(item, context, nextDepth);
|
ScanItem(item, elementWrapper, context, nextDepth);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (value is IEnumerable enumerable)
|
else if (value is IEnumerable enumerable)
|
||||||
|
|
@ -57,7 +63,7 @@ public static partial class AcBinarySerializer
|
||||||
foreach (var item in enumerable)
|
foreach (var item in enumerable)
|
||||||
{
|
{
|
||||||
if (item != null)
|
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
|
// Recursive scan on reference properties only
|
||||||
// Use typed getter for strings (much faster than reflection GetValue)
|
// Use typed getter for strings (much faster than reflection GetValue)
|
||||||
var refProperties = metadata.ReferenceProperties;
|
var refProperties = metadata.ReferenceProperties;
|
||||||
|
var hasPropertyFilter = context.HasPropertyFilter;
|
||||||
var nextDepth2 = depth + 1;
|
var nextDepth2 = depth + 1;
|
||||||
for (var i = 0; i < refProperties.Length; i++)
|
for (var i = 0; i < refProperties.Length; i++)
|
||||||
{
|
{
|
||||||
var prop = refProperties[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)
|
if (prop.AccessorType == PropertyAccessorType.String)
|
||||||
{
|
{
|
||||||
// Fast path: typed getter for string
|
// Fast path: typed getter for string
|
||||||
|
|
@ -127,10 +139,11 @@ public static partial class AcBinarySerializer
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[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
|
where TOutput : struct, IBinaryOutputBase
|
||||||
{
|
{
|
||||||
// String fast path — avoid GetWrapper entirely
|
// String fast path — avoid GetWrapper entirely
|
||||||
|
|
@ -141,7 +154,7 @@ public static partial class AcBinarySerializer
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var itemWrapper = context.GetWrapper(item.GetType());
|
var itemWrapper = item.GetType() == elementWrapper!.Metadata.SourceType ? elementWrapper : context.GetWrapper(item.GetType());
|
||||||
ScanValue(item, itemWrapper, context, depth);
|
ScanValue(item, itemWrapper, context, depth);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue