Unify and optimize primitive array/list serialization
Refactor AcBinarySerializer to use ReadOnlySpan<T> for bulk writing of primitive arrays and List<T>, replacing multiple specialized methods with a single TryWritePrimitiveCollection. This improves efficiency and reduces code duplication. Change default string interning mode to Attribute (opt-in). Update generated code path to allow reference tracking but not string interning. Adjust benchmarks to test correct serializer options. Reorder options for clarity.
This commit is contained in:
parent
dcd44cf705
commit
7284856dda
|
|
@ -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),
|
||||||
|
|
||||||
// MemoryPack
|
// MemoryPack
|
||||||
new MemoryPackBenchmark(testData.Order, SerializerMemoryPack),
|
new MemoryPackBenchmark(testData.Order, SerializerMemoryPack),
|
||||||
|
|
|
||||||
|
|
@ -558,44 +558,44 @@ public static partial class AcBinarySerializer
|
||||||
|
|
||||||
#region Bulk Array Writes — inline
|
#region Bulk Array Writes — inline
|
||||||
|
|
||||||
public void WriteDoubleArrayBulk(double[] array)
|
public void WriteDoubleArrayBulk(ReadOnlySpan<double> span)
|
||||||
{
|
{
|
||||||
EnsureCapacity(array.Length * 9);
|
EnsureCapacity(span.Length * 9);
|
||||||
for (var i = 0; i < array.Length; i++)
|
for (var i = 0; i < span.Length; i++)
|
||||||
{
|
{
|
||||||
_buffer[_position++] = BinaryTypeCode.Float64;
|
_buffer[_position++] = BinaryTypeCode.Float64;
|
||||||
Unsafe.WriteUnaligned(ref _buffer[_position], array[i]);
|
Unsafe.WriteUnaligned(ref _buffer[_position], span[i]);
|
||||||
_position += 8;
|
_position += 8;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void WriteFloatArrayBulk(float[] array)
|
public void WriteFloatArrayBulk(ReadOnlySpan<float> span)
|
||||||
{
|
{
|
||||||
EnsureCapacity(array.Length * 5);
|
EnsureCapacity(span.Length * 5);
|
||||||
for (var i = 0; i < array.Length; i++)
|
for (var i = 0; i < span.Length; i++)
|
||||||
{
|
{
|
||||||
_buffer[_position++] = BinaryTypeCode.Float32;
|
_buffer[_position++] = BinaryTypeCode.Float32;
|
||||||
Unsafe.WriteUnaligned(ref _buffer[_position], array[i]);
|
Unsafe.WriteUnaligned(ref _buffer[_position], span[i]);
|
||||||
_position += 4;
|
_position += 4;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void WriteGuidArrayBulk(Guid[] array)
|
public void WriteGuidArrayBulk(ReadOnlySpan<Guid> span)
|
||||||
{
|
{
|
||||||
EnsureCapacity(array.Length * 17);
|
EnsureCapacity(span.Length * 17);
|
||||||
for (var i = 0; i < array.Length; i++)
|
for (var i = 0; i < span.Length; i++)
|
||||||
{
|
{
|
||||||
_buffer[_position++] = BinaryTypeCode.Guid;
|
_buffer[_position++] = BinaryTypeCode.Guid;
|
||||||
array[i].TryWriteBytes(_buffer.AsSpan(_position, 16));
|
span[i].TryWriteBytes(_buffer.AsSpan(_position, 16));
|
||||||
_position += 16;
|
_position += 16;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void WriteInt32ArrayOptimized(int[] array)
|
public void WriteInt32ArrayOptimized(ReadOnlySpan<int> span)
|
||||||
{
|
{
|
||||||
for (var i = 0; i < array.Length; i++)
|
for (var i = 0; i < span.Length; i++)
|
||||||
{
|
{
|
||||||
var value = array[i];
|
var value = span[i];
|
||||||
if (BinaryTypeCode.TryEncodeTinyInt(value, out var tiny))
|
if (BinaryTypeCode.TryEncodeTinyInt(value, out var tiny))
|
||||||
{
|
{
|
||||||
WriteByte(tiny);
|
WriteByte(tiny);
|
||||||
|
|
@ -608,11 +608,11 @@ public static partial class AcBinarySerializer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void WriteLongArrayOptimized(long[] array)
|
public void WriteLongArrayOptimized(ReadOnlySpan<long> span)
|
||||||
{
|
{
|
||||||
for (var i = 0; i < array.Length; i++)
|
for (var i = 0; i < span.Length; i++)
|
||||||
{
|
{
|
||||||
var value = array[i];
|
var value = span[i];
|
||||||
if (value >= int.MinValue && value <= int.MaxValue)
|
if (value >= int.MinValue && value <= int.MaxValue)
|
||||||
{
|
{
|
||||||
var intValue = (int)value;
|
var intValue = (int)value;
|
||||||
|
|
|
||||||
|
|
@ -1047,12 +1047,13 @@ public static partial class AcBinarySerializer
|
||||||
var hasPropertyFilter = context.HasPropertyFilter;
|
var hasPropertyFilter = context.HasPropertyFilter;
|
||||||
|
|
||||||
// Source-generated fast path: bypass the entire switch/delegate loop.
|
// Source-generated fast path: bypass the entire switch/delegate loop.
|
||||||
// Only when no caching features are active (no string interning, no reference handling)
|
// Reference handling is safe: ref tracking happens in WriteObject (before WriteProperties)
|
||||||
// to avoid scan pass / write pass mismatch with interned strings and tracked references.
|
// and child objects go through WriteValueGenerated → WriteObject → runtime ref tracking.
|
||||||
|
// String interning is NOT safe: generated code doesn't set StringInternEligible → cursor mismatch.
|
||||||
if (context.UseGeneratedCode)
|
if (context.UseGeneratedCode)
|
||||||
{
|
{
|
||||||
var generatedWriter = wrapper.GeneratedWriter;
|
var generatedWriter = wrapper.GeneratedWriter;
|
||||||
if (generatedWriter != null && !hasPropertyFilter && !context.UseMetadata && !context.HasCaching)
|
if (generatedWriter != null && !hasPropertyFilter && !context.UseMetadata && !context.UseStringInterning)
|
||||||
{
|
{
|
||||||
generatedWriter.WriteProperties(value, context, nextDepth);
|
generatedWriter.WriteProperties(value, context, nextDepth);
|
||||||
return;
|
return;
|
||||||
|
|
@ -1493,7 +1494,7 @@ public static partial class AcBinarySerializer
|
||||||
#region Specialized Array Writers
|
#region Specialized Array Writers
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Optimized array writer with specialized paths for primitive arrays.
|
/// Optimized array writer with specialized paths for primitive collections.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static void WriteArray<TOutput>(IEnumerable enumerable, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int depth)
|
private static void WriteArray<TOutput>(IEnumerable enumerable, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int depth)
|
||||||
where TOutput : struct, IBinaryOutputBase
|
where TOutput : struct, IBinaryOutputBase
|
||||||
|
|
@ -1505,10 +1506,10 @@ public static partial class AcBinarySerializer
|
||||||
var metadata = wrapper.Metadata;
|
var metadata = wrapper.Metadata;
|
||||||
var elementType = metadata.CollectionElementType;
|
var elementType = metadata.CollectionElementType;
|
||||||
|
|
||||||
// Optimized path for primitive arrays
|
// Optimized path for primitive T[] and List<T> — unified via ReadOnlySpan<T>
|
||||||
if (elementType != null && metadata.SourceType.IsArray)
|
if (elementType != null)
|
||||||
{
|
{
|
||||||
if (TryWritePrimitiveArray(enumerable, elementType, context))
|
if (TryWritePrimitiveCollection(enumerable, elementType, context))
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1542,18 +1543,123 @@ public static partial class AcBinarySerializer
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Specialized array writer for primitive arrays using bulk memory operations.
|
/// Unified primitive collection writer for both T[] and List<T>.
|
||||||
|
/// Extracts ReadOnlySpan<T> from either source (zero-cost implicit conversion for arrays,
|
||||||
|
/// CollectionsMarshal.AsSpan for lists) and writes using bulk context methods with EnsureCapacity.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static bool TryWritePrimitiveArray<TOutput>(IEnumerable enumerable, Type elementType, BinarySerializationContext<TOutput> context)
|
private static bool TryWritePrimitiveCollection<TOutput>(IEnumerable enumerable, Type elementType, BinarySerializationContext<TOutput> context)
|
||||||
where TOutput : struct, IBinaryOutputBase
|
where TOutput : struct, IBinaryOutputBase
|
||||||
{
|
{
|
||||||
// String array needs context for interning — keep generic path
|
if (ReferenceEquals(elementType, IntType))
|
||||||
if (ReferenceEquals(elementType, StringType) && enumerable is string[] stringArray)
|
|
||||||
{
|
{
|
||||||
context.WriteVarUInt((uint)stringArray.Length);
|
ReadOnlySpan<int> span;
|
||||||
for (var i = 0; i < stringArray.Length; i++)
|
if (enumerable is int[] arr) span = arr;
|
||||||
|
else if (enumerable is List<int> list) span = CollectionsMarshal.AsSpan(list);
|
||||||
|
else return false;
|
||||||
|
|
||||||
|
context.WriteVarUInt((uint)span.Length);
|
||||||
|
context.WriteInt32ArrayOptimized(span);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ReferenceEquals(elementType, LongType))
|
||||||
{
|
{
|
||||||
var s = stringArray[i];
|
ReadOnlySpan<long> span;
|
||||||
|
if (enumerable is long[] arr) span = arr;
|
||||||
|
else if (enumerable is List<long> list) span = CollectionsMarshal.AsSpan(list);
|
||||||
|
else return false;
|
||||||
|
|
||||||
|
context.WriteVarUInt((uint)span.Length);
|
||||||
|
context.WriteLongArrayOptimized(span);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ReferenceEquals(elementType, DoubleType))
|
||||||
|
{
|
||||||
|
ReadOnlySpan<double> span;
|
||||||
|
if (enumerable is double[] arr) span = arr;
|
||||||
|
else if (enumerable is List<double> list) span = CollectionsMarshal.AsSpan(list);
|
||||||
|
else return false;
|
||||||
|
|
||||||
|
context.WriteVarUInt((uint)span.Length);
|
||||||
|
context.WriteDoubleArrayBulk(span);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ReferenceEquals(elementType, FloatType))
|
||||||
|
{
|
||||||
|
ReadOnlySpan<float> span;
|
||||||
|
if (enumerable is float[] arr) span = arr;
|
||||||
|
else if (enumerable is List<float> list) span = CollectionsMarshal.AsSpan(list);
|
||||||
|
else return false;
|
||||||
|
|
||||||
|
context.WriteVarUInt((uint)span.Length);
|
||||||
|
context.WriteFloatArrayBulk(span);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ReferenceEquals(elementType, BoolType))
|
||||||
|
{
|
||||||
|
ReadOnlySpan<bool> span;
|
||||||
|
if (enumerable is bool[] arr) span = arr;
|
||||||
|
else if (enumerable is List<bool> list) span = CollectionsMarshal.AsSpan(list);
|
||||||
|
else return false;
|
||||||
|
|
||||||
|
context.WriteVarUInt((uint)span.Length);
|
||||||
|
for (var i = 0; i < span.Length; i++)
|
||||||
|
context.WriteByte(span[i] ? BinaryTypeCode.True : BinaryTypeCode.False);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ReferenceEquals(elementType, GuidType))
|
||||||
|
{
|
||||||
|
ReadOnlySpan<Guid> span;
|
||||||
|
if (enumerable is Guid[] arr) span = arr;
|
||||||
|
else if (enumerable is List<Guid> list) span = CollectionsMarshal.AsSpan(list);
|
||||||
|
else return false;
|
||||||
|
|
||||||
|
context.WriteVarUInt((uint)span.Length);
|
||||||
|
context.WriteGuidArrayBulk(span);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ReferenceEquals(elementType, DecimalType))
|
||||||
|
{
|
||||||
|
ReadOnlySpan<decimal> span;
|
||||||
|
if (enumerable is decimal[] arr) span = arr;
|
||||||
|
else if (enumerable is List<decimal> list) span = CollectionsMarshal.AsSpan(list);
|
||||||
|
else return false;
|
||||||
|
|
||||||
|
context.WriteVarUInt((uint)span.Length);
|
||||||
|
for (var i = 0; i < span.Length; i++)
|
||||||
|
WriteDecimalUnsafe(span[i], context);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ReferenceEquals(elementType, DateTimeType))
|
||||||
|
{
|
||||||
|
ReadOnlySpan<DateTime> span;
|
||||||
|
if (enumerable is DateTime[] arr) span = arr;
|
||||||
|
else if (enumerable is List<DateTime> list) span = CollectionsMarshal.AsSpan(list);
|
||||||
|
else return false;
|
||||||
|
|
||||||
|
context.WriteVarUInt((uint)span.Length);
|
||||||
|
for (var i = 0; i < span.Length; i++)
|
||||||
|
WriteDateTimeUnsafe(span[i], context);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ReferenceEquals(elementType, StringType))
|
||||||
|
{
|
||||||
|
ReadOnlySpan<string?> span;
|
||||||
|
if (enumerable is string?[] arr) span = arr;
|
||||||
|
else if (enumerable is List<string?> list) span = CollectionsMarshal.AsSpan(list);
|
||||||
|
else return false;
|
||||||
|
|
||||||
|
context.WriteVarUInt((uint)span.Length);
|
||||||
|
for (var i = 0; i < span.Length; i++)
|
||||||
|
{
|
||||||
|
var s = span[i];
|
||||||
if (s == null)
|
if (s == null)
|
||||||
context.WriteByte(BinaryTypeCode.Null);
|
context.WriteByte(BinaryTypeCode.Null);
|
||||||
else
|
else
|
||||||
|
|
@ -1562,89 +1668,6 @@ public static partial class AcBinarySerializer
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// All other primitive arrays — inline write through context (zero virtual dispatch)
|
|
||||||
return TryWritePrimitiveArrayCore(enumerable, elementType, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Core primitive array writes. Uses context write methods (zero virtual dispatch).
|
|
||||||
/// </summary>
|
|
||||||
private static bool TryWritePrimitiveArrayCore<TOutput>(IEnumerable enumerable, Type elementType, BinarySerializationContext<TOutput> context)
|
|
||||||
where TOutput : struct, IBinaryOutputBase
|
|
||||||
{
|
|
||||||
// Int32 array - very common case
|
|
||||||
if (ReferenceEquals(elementType, IntType) && enumerable is int[] intArray)
|
|
||||||
{
|
|
||||||
context.WriteVarUInt((uint)intArray.Length);
|
|
||||||
context.WriteInt32ArrayOptimized(intArray);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Double array - bulk write as raw bytes
|
|
||||||
if (ReferenceEquals(elementType, DoubleType) && enumerable is double[] doubleArray)
|
|
||||||
{
|
|
||||||
context.WriteVarUInt((uint)doubleArray.Length);
|
|
||||||
context.WriteDoubleArrayBulk(doubleArray);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Long array
|
|
||||||
if (ReferenceEquals(elementType, LongType) && enumerable is long[] longArray)
|
|
||||||
{
|
|
||||||
context.WriteVarUInt((uint)longArray.Length);
|
|
||||||
context.WriteLongArrayOptimized(longArray);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Float array - bulk write as raw bytes
|
|
||||||
if (ReferenceEquals(elementType, FloatType) && enumerable is float[] floatArray)
|
|
||||||
{
|
|
||||||
context.WriteVarUInt((uint)floatArray.Length);
|
|
||||||
context.WriteFloatArrayBulk(floatArray);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bool array - pack as bytes
|
|
||||||
if (ReferenceEquals(elementType, BoolType) && enumerable is bool[] boolArray)
|
|
||||||
{
|
|
||||||
context.WriteVarUInt((uint)boolArray.Length);
|
|
||||||
for (var i = 0; i < boolArray.Length; i++)
|
|
||||||
{
|
|
||||||
context.WriteByte(boolArray[i] ? BinaryTypeCode.True : BinaryTypeCode.False);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Guid array - bulk write
|
|
||||||
if (ReferenceEquals(elementType, GuidType) && enumerable is Guid[] guidArray)
|
|
||||||
{
|
|
||||||
context.WriteVarUInt((uint)guidArray.Length);
|
|
||||||
context.WriteGuidArrayBulk(guidArray);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decimal array
|
|
||||||
if (ReferenceEquals(elementType, DecimalType) && enumerable is decimal[] decimalArray)
|
|
||||||
{
|
|
||||||
context.WriteVarUInt((uint)decimalArray.Length);
|
|
||||||
for (var i = 0; i < decimalArray.Length; i++)
|
|
||||||
{
|
|
||||||
WriteDecimalUnsafe(decimalArray[i], context);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// DateTime array
|
|
||||||
if (ReferenceEquals(elementType, DateTimeType) && enumerable is DateTime[] dateTimeArray)
|
|
||||||
{
|
|
||||||
context.WriteVarUInt((uint)dateTimeArray.Length);
|
|
||||||
for (var i = 0; i < dateTimeArray.Length; i++)
|
|
||||||
{
|
|
||||||
WriteDateTimeUnsafe(dateTimeArray[i], context);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -86,14 +86,6 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
|
||||||
|
|
||||||
public bool UseGeneratedCode { get; set; } = true;
|
public bool UseGeneratedCode { get; set; } = true;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// When true, checks for duplicate property name hashes during serialization (UseMetadata mode).
|
|
||||||
/// Throws exception if FNV-1a hash collision is detected between property names of the same type.
|
|
||||||
/// Should be enabled during development/testing, can be disabled in production for performance.
|
|
||||||
/// Default: true (safety first)
|
|
||||||
/// </summary>
|
|
||||||
public bool CheckDuplicatePropName { get; init; } = true;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Controls how string interning is applied during serialization.
|
/// Controls how string interning is applied during serialization.
|
||||||
/// None: No interning, all strings written inline.
|
/// None: No interning, all strings written inline.
|
||||||
|
|
@ -101,7 +93,15 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
|
||||||
/// All: All strings within length limits are interned (legacy behavior).
|
/// All: All strings within length limits are interned (legacy behavior).
|
||||||
/// Default: All
|
/// Default: All
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public StringInterningMode UseStringInterning { get; set; } = StringInterningMode.All;
|
public StringInterningMode UseStringInterning { get; set; } = StringInterningMode.Attribute;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When true, checks for duplicate property name hashes during serialization (UseMetadata mode).
|
||||||
|
/// Throws exception if FNV-1a hash collision is detected between property names of the same type.
|
||||||
|
/// Should be enabled during development/testing, can be disabled in production for performance.
|
||||||
|
/// Default: true (safety first)
|
||||||
|
/// </summary>
|
||||||
|
public bool CheckDuplicatePropName { get; init; } = true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Minimum string length to consider for interning.
|
/// Minimum string length to consider for interning.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue