Reduce pool sizes, optimize IdentityMap, add config option

Reduced default pool sizes from 16 to 8 for serializers and object pools, now configurable via AcSerializerOptions.MaxContextPoolSize. Improved IdentityMap<TId> memory usage and cache locality by shrinking small int bitmap/array. Refactored hash table logic to use cached bucket length. Optimized Reset to clear only used entries and adjusted array pooling. String key equality now always uses ordinal comparison. Updated context pool logic to respect per-serializer pool size. Includes minor code cleanups and comments.
This commit is contained in:
Loretta 2026-01-31 17:17:51 +01:00
parent dbacc2da80
commit c7f44906e7
8 changed files with 63 additions and 31 deletions

View File

@ -295,7 +295,7 @@ internal sealed class DefaultObjectPool<T> : ObjectPool<T> where T : class
[ThreadStatic] private static T? _threadLocalItem;
private readonly ConcurrentQueue<T> _pool = new();
private readonly IPooledObjectPolicy<T> _policy;
private const int MaxPoolSize = 16;
private const int MaxPoolSize = 8;
public DefaultObjectPool(IPooledObjectPolicy<T> policy) => _policy = policy;

View File

@ -5,6 +5,7 @@ namespace AyCode.Core.Serializers;
public abstract class AcSerializerOptions
{
public int MaxContextPoolSize { get; init; } = 8;
public abstract AcSerializerType SerializerType { get; init; }
/// <summary>
@ -27,7 +28,7 @@ public abstract class AcSerializerOptions
public bool UseAsync
{
get => _useAsync && ReferenceHandling != ReferenceHandlingMode.None;
get => _useAsync && (ReferenceHandling != ReferenceHandlingMode.None); //|| UseStringIntern)
init => _useAsync = !DetectedIsWasm && value;
}

View File

@ -17,7 +17,6 @@ public static partial class AcBinarySerializer
private static class BinarySerializationContextPool
{
private static readonly ConcurrentQueue<BinarySerializationContext> Pool = new();
private const int MaxPoolSize = 16;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static BinarySerializationContext Get(AcBinarySerializerOptions options)
@ -40,7 +39,7 @@ public static partial class AcBinarySerializer
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Return(BinarySerializationContext context)
{
if (Pool.Count < MaxPoolSize)
if (Pool.Count < context.Options.MaxContextPoolSize)
{
context.Clear();
Pool.Enqueue(context);

View File

@ -1,4 +1,7 @@
using System.Buffers;
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@ -45,9 +48,12 @@ public sealed class IdentityMap<TId> : IIdentityMap where TId : notnull
{
private bool _useSmallInt = false;
private const int SmallBitmapSize = 64;
private const int SmallSize = 4096;
// Small int optimization (TId = int only, 0-65535)
private const int SmallBitmapSize = 1024;
private const int SmallSize = SmallBitmapSize * 64;
//private const int SmallBitmapSize = 1024;
//private const int SmallSize = SmallBitmapSize * 64;
// Small int optimization (TId = int only, 0-4095)
//private const int SmallSize = 4096;
@ -85,6 +91,7 @@ public sealed class IdentityMap<TId> : IIdentityMap where TId : notnull
{
}
/// <summary>
/// Tries to add a key to tracking (serialization).
/// Returns true if first occurrence (key was added).
@ -141,8 +148,10 @@ public sealed class IdentityMap<TId> : IIdentityMap where TId : notnull
{
var strA = Unsafe.As<TId, string>(ref a);
var strB = Unsafe.As<TId, string>(ref b);
// Fast path: reference equality (interned strings)
if (ReferenceEquals(strA, strB)) return true;
//if (ReferenceEquals(strA, strB)) return true;
// Ordinal comparison is fastest for non-interned
return string.Equals(strA, strB, StringComparison.Ordinal);
}
@ -169,7 +178,7 @@ public sealed class IdentityMap<TId> : IIdentityMap where TId : notnull
InitHashTable(InitialHashCapacity);
}
var bucketIdx = (hash & 0x7FFFFFFF) % _buckets!.Length;
var bucketIdx = (hash & 0x7FFFFFFF) % _bucketsLength;
// Search chain
for (var i = _buckets[bucketIdx]; i >= 0; i = _entries![i].Next)
@ -205,6 +214,7 @@ public sealed class IdentityMap<TId> : IIdentityMap where TId : notnull
if (IsInt64) return Unsafe.As<TId, long>(ref key).GetHashCode();
if (IsGuid) return Unsafe.As<TId, Guid>(ref key).GetHashCode();
if (IsString) return string.GetHashCode(Unsafe.As<TId, string>(ref key), StringComparison.Ordinal);
return key.GetHashCode();
}
@ -219,8 +229,10 @@ public sealed class IdentityMap<TId> : IIdentityMap where TId : notnull
_entries = ArrayPool<HashSlot>.Shared.Rent(actualCapacity);
_entriesLength = actualCapacity;
//Array.Clear(_entries, 0, _entriesLength);
_keys = ArrayPool<TId>.Shared.Rent(actualCapacity);
//Array.Clear(_keys, 0, actualCapacity);
_count = 0;
}
@ -278,7 +290,7 @@ public sealed class IdentityMap<TId> : IIdentityMap where TId : notnull
if (_buckets == null) return false;
var hash = GetHashCode(key);
var bucketIdx = (hash & 0x7FFFFFFF) % _buckets.Length;
var bucketIdx = (hash & 0x7FFFFFFF) % _bucketsLength;
for (var i = _buckets[bucketIdx]; i >= 0; i = _entries![i].Next)
{
@ -334,6 +346,7 @@ public sealed class IdentityMap<TId> : IIdentityMap where TId : notnull
if (_smallValues == null)
{
_smallValues = ArrayPool<object?>.Shared.Rent(SmallSize);
Array.Clear(_smallValues, 0, SmallSize);
}
var wordIdx = key >> 6;
@ -400,6 +413,7 @@ public sealed class IdentityMap<TId> : IIdentityMap where TId : notnull
if (_smallValues == null)
{
_smallValues = ArrayPool<object?>.Shared.Rent(SmallSize);
Array.Clear(_smallValues, 0, SmallSize);
}
var wordIdx = key >> 6;
@ -458,7 +472,7 @@ public sealed class IdentityMap<TId> : IIdentityMap where TId : notnull
}
var hash = GetHashCode(key);
var bucketIdx = (hash & 0x7FFFFFFF) % _buckets.Length;
var bucketIdx = (hash & 0x7FFFFFFF) % _bucketsLength;
for (var i = _buckets[bucketIdx]; i >= 0; i = _entries![i].Next)
{
@ -480,17 +494,36 @@ public sealed class IdentityMap<TId> : IIdentityMap where TId : notnull
/// <param name="preRentBuckets">If true, pre-rent arrays at next capacity (useful for async Clear to shift work from hot path)</param>
public void Reset(bool preRentBuckets = false)
{
if (_smallBitmap != null)
{
ArrayPool<ulong>.Shared.Return(_smallBitmap);
_smallBitmap = null;
}
if (_smallValues != null)
{
ArrayPool<object?>.Shared.Return(_smallValues, clearArray: true); // Clear to release object refs
_smallValues = null;
//Array.Clear(_smallValues, 0, SmallSize);
// Végigmegyünk a bitmap-en és csak a set bit-ekhez tartozó értékeket töröljük
for (var wordIdx = 0; wordIdx < SmallBitmapSize; wordIdx++)
{
var word = _smallBitmap[wordIdx];
if (word == 0) continue;
var baseIdx = wordIdx << 6;
while (word != 0)
{
var bitPos = BitOperations.TrailingZeroCount(word);
_smallValues[baseIdx + bitPos] = null;
word &= word - 1; // Clear lowest set bit
}
}
//ArrayPool<object?>.Shared.Return(_smallValues, clearArray: true); // Clear to release object refs
//_smallValues = null;
}
if (_smallBitmap != null)
{
Array.Clear(_smallBitmap, 0, SmallBitmapSize);
//ArrayPool<ulong>.Shared.Return(_smallBitmap);
//_smallBitmap = null;
}
if (_buckets != null)
{
// Small arrays: keep and clear (faster than pool round-trip)
@ -508,8 +541,8 @@ public sealed class IdentityMap<TId> : IIdentityMap where TId : notnull
}
// Large arrays: return to pool, remember half capacity
var nextCapacity = Math.Max(_bucketsLength / 2, InitialHashCapacity);
var nextCapacity = Math.Max(_bucketsLength / 2, InitialHashCapacity * 5);
// Clear entries/keys to release object references before returning to pool
// Otherwise pool holds refs → GC can't collect!
if (_count > 0)
@ -517,10 +550,10 @@ public sealed class IdentityMap<TId> : IIdentityMap where TId : notnull
Array.Clear(_entries!, 0, _count);
Array.Clear(_keys!, 0, _count);
}
ArrayPool<int>.Shared.Return(_buckets);
ArrayPool<HashSlot>.Shared.Return(_entries!);
ArrayPool<TId>.Shared.Return(_keys!);
ArrayPool<HashSlot>.Shared.Return(_entries!,false);
ArrayPool<TId>.Shared.Return(_keys!, false);
if (preRentBuckets)
{
@ -531,8 +564,10 @@ public sealed class IdentityMap<TId> : IIdentityMap where TId : notnull
_entries = ArrayPool<HashSlot>.Shared.Rent(nextCapacity);
_entriesLength = nextCapacity;
//Array.Clear(_entries, 0, _entriesLength);
_keys = ArrayPool<TId>.Shared.Rent(nextCapacity);
//Array.Clear(_keys, 0, nextCapacity);
}
else
{

View File

@ -8,7 +8,6 @@ public static partial class AcJsonDeserializer
private static class JsonDeserializationContextPool
{
private static readonly ConcurrentQueue<DeserializationContext> Pool = new();
private const int MaxPoolSize = 16;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static DeserializationContext Get(in AcJsonSerializerOptions options)
@ -24,7 +23,7 @@ public static partial class AcJsonDeserializer
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Return(DeserializationContext context, in AcJsonSerializerOptions options)
{
if (Pool.Count < MaxPoolSize)
if (Pool.Count < options.MaxContextPoolSize)
{
context.Clear(options);
Pool.Enqueue(context);

View File

@ -621,7 +621,7 @@ public static partial class AcJsonDeserializer
if (_context != null)
{
JsonDeserializationContextPool.Return(_context, null);
JsonDeserializationContextPool.Return(_context, _context.Options);
_context = null;
}
_document?.Dispose();

View File

@ -13,7 +13,6 @@ public static partial class AcJsonSerializer
private static class SerializationContextPool
{
private static readonly ConcurrentQueue<JsonSerializationContext> Pool = new();
private const int MaxPoolSize = 16;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static JsonSerializationContext Get(in AcJsonSerializerOptions options)
@ -36,7 +35,7 @@ public static partial class AcJsonSerializer
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Return(JsonSerializationContext context)
{
if (Pool.Count < MaxPoolSize)
if (Pool.Count < context.Options.MaxContextPoolSize)
{
context.Clear();
Pool.Enqueue(context);

View File

@ -16,7 +16,6 @@ public static partial class AcToonSerializer
private static class ToonSerializationContextPool
{
private static readonly ConcurrentQueue<ToonSerializationContext> Pool = new();
private const int MaxPoolSize = 16;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ToonSerializationContext Get(AcToonSerializerOptions options)
@ -32,7 +31,7 @@ public static partial class AcToonSerializer
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Return(ToonSerializationContext context)
{
if (Pool.Count < MaxPoolSize)
if (Pool.Count < context.Options.MaxContextPoolSize)
{
context.Clear();
Pool.Enqueue(context);