Refactor string interning to use enum and attribute

Replaces boolean UseStringInterning with StringInterningMode enum for more granular control (None, Attribute, All). Introduces AcStringInternAttribute for per-property interning configuration. Updates all usages and documentation to reflect the new approach, ensuring explicit and flexible string interning behavior in serialization. Default mode is now All to preserve legacy behavior.
This commit is contained in:
Loretta 2026-01-26 11:04:25 +01:00
parent 1a77ee4bf9
commit e73fd7ae4a
10 changed files with 70 additions and 20 deletions

View File

@ -535,7 +535,7 @@ public abstract class AcBinaryOptionsBenchmarkBase
BinaryBenchmarkMode.FastMode => new AcBinarySerializerOptions BinaryBenchmarkMode.FastMode => new AcBinarySerializerOptions
{ {
UseMetadata = false, UseMetadata = false,
UseStringInterning = false, UseStringInterning = StringInterningMode.None,
ReferenceHandling = ReferenceHandlingMode.None, ReferenceHandling = ReferenceHandlingMode.None,
}, },
_ => new AcBinarySerializerOptions() _ => new AcBinarySerializerOptions()

View File

@ -121,7 +121,7 @@ public static class Program
sharedUser: sharedUser); sharedUser: sharedUser);
var options = AcBinarySerializerOptions.WithoutReferenceHandling; var options = AcBinarySerializerOptions.WithoutReferenceHandling;
options.UseStringInterning = false; options.UseStringInterning = StringInterningMode.None;
// Warmup (fills caches) // Warmup (fills caches)
System.Console.WriteLine("Warming up (10 iterations)..."); System.Console.WriteLine("Warming up (10 iterations)...");
@ -419,7 +419,7 @@ public static class Program
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, AcBinarySerializerOptions.FastMode, SerializerAcBinaryFastMode),
new AcBinaryBenchmark(testData.Order, new AcBinarySerializerOptions { UseStringInterning = false }, SerializerAcBinaryNoIntern), new AcBinaryBenchmark(testData.Order, new AcBinarySerializerOptions { UseStringInterning = StringInterningMode.None }, SerializerAcBinaryNoIntern),
// AcJson // AcJson
new AcJsonBenchmark(testData.Order, AcJsonSerializerOptions.Default, SerializerAcJsonDefault), new AcJsonBenchmark(testData.Order, AcJsonSerializerOptions.Default, SerializerAcJsonDefault),

View File

@ -62,7 +62,7 @@ public class AcBinarySerializerBenchmarkTests
var order = TestDataFactory.CreateBenchmarkOrder(itemCount: 5, palletsPerItem: 3, measurementsPerPallet: 2, pointsPerMeasurement: 5); var order = TestDataFactory.CreateBenchmarkOrder(itemCount: 5, palletsPerItem: 3, measurementsPerPallet: 2, pointsPerMeasurement: 5);
var binaryWithInterning = AcBinarySerializer.Serialize(order, AcBinarySerializerOptions.Default); var binaryWithInterning = AcBinarySerializer.Serialize(order, AcBinarySerializerOptions.Default);
var binaryWithoutInterning = AcBinarySerializer.Serialize(order, new AcBinarySerializerOptions { UseStringInterning = false }); var binaryWithoutInterning = AcBinarySerializer.Serialize(order, new AcBinarySerializerOptions { UseStringInterning = StringInterningMode.None });
// Note: String interning may not always result in smaller size due to header overhead // Note: String interning may not always result in smaller size due to header overhead
// The primary benefit is for larger datasets with many repeated strings // The primary benefit is for larger datasets with many repeated strings

View File

@ -24,7 +24,7 @@ public class AcBinarySerializerStringInterningTests
var binaryWithInterning = AcBinarySerializer.Serialize(obj, AcBinarySerializerOptions.Default); var binaryWithInterning = AcBinarySerializer.Serialize(obj, AcBinarySerializerOptions.Default);
var binaryWithoutInterning = AcBinarySerializer.Serialize(obj, var binaryWithoutInterning = AcBinarySerializer.Serialize(obj,
new AcBinarySerializerOptions { UseStringInterning = false }); new AcBinarySerializerOptions { UseStringInterning = StringInterningMode.None });
Assert.IsTrue(binaryWithInterning.Length < binaryWithoutInterning.Length, Assert.IsTrue(binaryWithInterning.Length < binaryWithoutInterning.Length,
$"With interning: {binaryWithInterning.Length}, Without: {binaryWithoutInterning.Length}"); $"With interning: {binaryWithInterning.Length}, Without: {binaryWithoutInterning.Length}");

View File

@ -197,7 +197,7 @@ public class QuickBenchmark
var withInterningMs = sw.Elapsed.TotalMilliseconds; var withInterningMs = sw.Elapsed.TotalMilliseconds;
// Without interning // Without interning
var noInternOptions = new AcBinarySerializerOptions { UseStringInterning = false }; var noInternOptions = new AcBinarySerializerOptions { UseStringInterning = StringInterningMode.None };
sw.Restart(); sw.Restart();
byte[] withoutInterning = null!; byte[] withoutInterning = null!;
for (int i = 0; i < iterations; i++) for (int i = 0; i < iterations; i++)
@ -427,7 +427,7 @@ public class QuickBenchmark
sharedMetadata: sharedMeta); sharedMetadata: sharedMeta);
var singleOptions = AcBinarySerializerOptions.FastMode; var singleOptions = AcBinarySerializerOptions.FastMode;
singleOptions.UseStringInterning = false; singleOptions.UseStringInterning = StringInterningMode.None;
Console.WriteLine("=== MINIMAL WARMUP TEST ==="); Console.WriteLine("=== MINIMAL WARMUP TEST ===");
Console.WriteLine(); Console.WriteLine();
@ -507,9 +507,9 @@ public class QuickBenchmark
// Options // Options
var withRefOptions = AcBinarySerializerOptions.Default; var withRefOptions = AcBinarySerializerOptions.Default;
//withRefOptions.UseStringInterning = false; //withRefOptions.UseStringInterning = StringInterningMode.None;
var noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling; var noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling;
noRefOptions.UseStringInterning = false; noRefOptions.UseStringInterning = StringInterningMode.None;
// Pre-serialize // Pre-serialize
var acBinaryWithRef = AcBinarySerializer.Serialize(testOrder, withRefOptions); var acBinaryWithRef = AcBinarySerializer.Serialize(testOrder, withRefOptions);

View File

@ -92,7 +92,7 @@ public static partial class AcBinarySerializer
#endif #endif
// These properties delegate to Options for convenience // These properties delegate to Options for convenience
public bool UseStringInterning => Options.UseStringInterning; public bool UseStringInterning => Options.UseStringInterning != StringInterningMode.None;
public bool UseMetadata => Options.UseMetadata; public bool UseMetadata => Options.UseMetadata;
public byte MinStringInternLength => Options.MinStringInternLength; public byte MinStringInternLength => Options.MinStringInternLength;
public byte MaxStringInternLength => Options.MaxStringInternLength; public byte MaxStringInternLength => Options.MaxStringInternLength;

View File

@ -60,9 +60,9 @@ public static partial class AcBinarySerializer
options ??= AcBinarySerializerOptions.Default; options ??= AcBinarySerializerOptions.Default;
// For analysis, use the provided reference handling mode // For analysis, use the provided reference handling mode
var analysisOptions = new AcBinarySerializerOptions var analysisOptions = new AcBinarySerializerOptions
{ {
UseStringInterning = true, UseStringInterning = StringInterningMode.All,
MinStringInternLength = options.MinStringInternLength, MinStringInternLength = options.MinStringInternLength,
MaxStringInternLength = options.MaxStringInternLength, MaxStringInternLength = options.MaxStringInternLength,
ReferenceHandling = options.ReferenceHandling ReferenceHandling = options.ReferenceHandling

View File

@ -33,7 +33,7 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
/// </summary> /// </summary>
public static AcBinarySerializerOptions FastMode => new() public static AcBinarySerializerOptions FastMode => new()
{ {
UseStringInterning = false, UseStringInterning = StringInterningMode.None,
ReferenceHandling = ReferenceHandlingMode.None ReferenceHandling = ReferenceHandlingMode.None
}; };
@ -44,7 +44,7 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
public static AcBinarySerializerOptions ShallowCopy => new() public static AcBinarySerializerOptions ShallowCopy => new()
{ {
MaxDepth = 0, MaxDepth = 0,
UseStringInterning = false, UseStringInterning = StringInterningMode.None,
ReferenceHandling = ReferenceHandlingMode.None ReferenceHandling = ReferenceHandlingMode.None
}; };
@ -89,12 +89,13 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
public bool UseMetadata { get; init; } = false; public bool UseMetadata { get; init; } = false;
/// <summary> /// <summary>
/// Whether to intern repeated strings. /// Controls how string interning is applied during serialization.
/// When enabled, duplicate strings are stored once and referenced by index. /// None: No interning, all strings written inline.
/// Reduces size and memory for objects with many repeated string values. /// Attribute: Only properties with [AcStringIntern(true)] are interned.
/// Default: true /// All: All strings within length limits are interned (legacy behavior).
/// Default: All
/// </summary> /// </summary>
public bool UseStringInterning { get; set; } = true; public StringInterningMode UseStringInterning { get; set; } = StringInterningMode.All;
/// <summary> /// <summary>
/// Minimum string length to consider for interning. /// Minimum string length to consider for interning.

View File

@ -0,0 +1,25 @@
namespace AyCode.Core.Serializers.Binaries;
/// <summary>
/// Controls string interning for a specific property during binary serialization.
/// When StringInterningMode is Attribute: only properties with [AcStringIntern(true)] are interned.
/// When StringInterningMode is All: properties with [AcStringIntern(false)] opt-out from interning.
/// Attribute is inherited from base class properties if not explicitly specified.
/// </summary>
[AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)]
public sealed class AcStringInternAttribute : Attribute
{
/// <summary>
/// Whether string interning is enabled for this property.
/// </summary>
public bool Enabled { get; }
/// <summary>
/// Creates a new AcStringInternAttribute.
/// </summary>
/// <param name="enabled">True to enable interning, false to disable.</param>
public AcStringInternAttribute(bool enabled)
{
Enabled = enabled;
}
}

View File

@ -0,0 +1,24 @@
namespace AyCode.Core.Serializers.Binaries;
/// <summary>
/// Controls string interning behavior during binary serialization.
/// </summary>
public enum StringInterningMode
{
/// <summary>
/// No string interning. All strings are written inline.
/// </summary>
None = 0,
/// <summary>
/// Only intern strings on properties marked with [AcStringIntern(true)].
/// Properties without attribute or with [AcStringIntern(false)] are written inline.
/// </summary>
Attribute = 1,
/// <summary>
/// Intern all strings that meet the length criteria (MinStringInternLength to MaxStringInternLength).
/// [AcStringIntern(false)] can opt-out specific properties.
/// </summary>
All = 2
}