From e73fd7ae4a71ec7fced5ede18505a4bc41c6ad81 Mon Sep 17 00:00:00 2001 From: Loretta Date: Mon, 26 Jan 2026 11:04:25 +0100 Subject: [PATCH] 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. --- AyCode.Benchmark/SerializationBenchmarks.cs | 2 +- AyCode.Core.Serializers.Console/Program.cs | 4 +-- .../AcBinarySerializerBenchmarkTests.cs | 2 +- .../AcBinarySerializerStringInterningTests.cs | 2 +- .../Serialization/QuickBenchmark.cs | 8 +++--- ...rySerializer.BinarySerializationContext.cs | 2 +- .../Binaries/AcBinarySerializer.cs | 6 ++--- .../Binaries/AcBinarySerializerOptions.cs | 15 +++++------ .../Binaries/AcStringInternAttribute.cs | 25 +++++++++++++++++++ .../Binaries/StringInterningMode.cs | 24 ++++++++++++++++++ 10 files changed, 70 insertions(+), 20 deletions(-) create mode 100644 AyCode.Core/Serializers/Binaries/AcStringInternAttribute.cs create mode 100644 AyCode.Core/Serializers/Binaries/StringInterningMode.cs diff --git a/AyCode.Benchmark/SerializationBenchmarks.cs b/AyCode.Benchmark/SerializationBenchmarks.cs index 471eecf..7fed174 100644 --- a/AyCode.Benchmark/SerializationBenchmarks.cs +++ b/AyCode.Benchmark/SerializationBenchmarks.cs @@ -535,7 +535,7 @@ public abstract class AcBinaryOptionsBenchmarkBase BinaryBenchmarkMode.FastMode => new AcBinarySerializerOptions { UseMetadata = false, - UseStringInterning = false, + UseStringInterning = StringInterningMode.None, ReferenceHandling = ReferenceHandlingMode.None, }, _ => new AcBinarySerializerOptions() diff --git a/AyCode.Core.Serializers.Console/Program.cs b/AyCode.Core.Serializers.Console/Program.cs index c9b461f..246fb91 100644 --- a/AyCode.Core.Serializers.Console/Program.cs +++ b/AyCode.Core.Serializers.Console/Program.cs @@ -121,7 +121,7 @@ public static class Program sharedUser: sharedUser); var options = AcBinarySerializerOptions.WithoutReferenceHandling; - options.UseStringInterning = false; + options.UseStringInterning = StringInterningMode.None; // Warmup (fills caches) 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.WithoutReferenceHandling, SerializerAcBinaryNoRef), 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 new AcJsonBenchmark(testData.Order, AcJsonSerializerOptions.Default, SerializerAcJsonDefault), diff --git a/AyCode.Core.Tests/Serialization/AcBinarySerializerBenchmarkTests.cs b/AyCode.Core.Tests/Serialization/AcBinarySerializerBenchmarkTests.cs index 7c0f194..03e538d 100644 --- a/AyCode.Core.Tests/Serialization/AcBinarySerializerBenchmarkTests.cs +++ b/AyCode.Core.Tests/Serialization/AcBinarySerializerBenchmarkTests.cs @@ -62,7 +62,7 @@ public class AcBinarySerializerBenchmarkTests var order = TestDataFactory.CreateBenchmarkOrder(itemCount: 5, palletsPerItem: 3, measurementsPerPallet: 2, pointsPerMeasurement: 5); 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 // The primary benefit is for larger datasets with many repeated strings diff --git a/AyCode.Core.Tests/Serialization/AcBinarySerializerStringInterningTests.cs b/AyCode.Core.Tests/Serialization/AcBinarySerializerStringInterningTests.cs index d71d16b..97a37d0 100644 --- a/AyCode.Core.Tests/Serialization/AcBinarySerializerStringInterningTests.cs +++ b/AyCode.Core.Tests/Serialization/AcBinarySerializerStringInterningTests.cs @@ -24,7 +24,7 @@ public class AcBinarySerializerStringInterningTests var binaryWithInterning = AcBinarySerializer.Serialize(obj, AcBinarySerializerOptions.Default); var binaryWithoutInterning = AcBinarySerializer.Serialize(obj, - new AcBinarySerializerOptions { UseStringInterning = false }); + new AcBinarySerializerOptions { UseStringInterning = StringInterningMode.None }); Assert.IsTrue(binaryWithInterning.Length < binaryWithoutInterning.Length, $"With interning: {binaryWithInterning.Length}, Without: {binaryWithoutInterning.Length}"); diff --git a/AyCode.Core.Tests/Serialization/QuickBenchmark.cs b/AyCode.Core.Tests/Serialization/QuickBenchmark.cs index 97c9ec1..2d9b9a6 100644 --- a/AyCode.Core.Tests/Serialization/QuickBenchmark.cs +++ b/AyCode.Core.Tests/Serialization/QuickBenchmark.cs @@ -197,7 +197,7 @@ public class QuickBenchmark var withInterningMs = sw.Elapsed.TotalMilliseconds; // Without interning - var noInternOptions = new AcBinarySerializerOptions { UseStringInterning = false }; + var noInternOptions = new AcBinarySerializerOptions { UseStringInterning = StringInterningMode.None }; sw.Restart(); byte[] withoutInterning = null!; for (int i = 0; i < iterations; i++) @@ -427,7 +427,7 @@ public class QuickBenchmark sharedMetadata: sharedMeta); var singleOptions = AcBinarySerializerOptions.FastMode; - singleOptions.UseStringInterning = false; + singleOptions.UseStringInterning = StringInterningMode.None; Console.WriteLine("=== MINIMAL WARMUP TEST ==="); Console.WriteLine(); @@ -507,9 +507,9 @@ public class QuickBenchmark // Options var withRefOptions = AcBinarySerializerOptions.Default; - //withRefOptions.UseStringInterning = false; + //withRefOptions.UseStringInterning = StringInterningMode.None; var noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling; - noRefOptions.UseStringInterning = false; + noRefOptions.UseStringInterning = StringInterningMode.None; // Pre-serialize var acBinaryWithRef = AcBinarySerializer.Serialize(testOrder, withRefOptions); diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs index 7ac0d3c..1628650 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs @@ -92,7 +92,7 @@ public static partial class AcBinarySerializer #endif // 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 byte MinStringInternLength => Options.MinStringInternLength; public byte MaxStringInternLength => Options.MaxStringInternLength; diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs index ec5d747..6de9d2a 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs @@ -60,9 +60,9 @@ public static partial class AcBinarySerializer options ??= AcBinarySerializerOptions.Default; // For analysis, use the provided reference handling mode - var analysisOptions = new AcBinarySerializerOptions - { - UseStringInterning = true, + var analysisOptions = new AcBinarySerializerOptions + { + UseStringInterning = StringInterningMode.All, MinStringInternLength = options.MinStringInternLength, MaxStringInternLength = options.MaxStringInternLength, ReferenceHandling = options.ReferenceHandling diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs index 767b510..4edf5c1 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs @@ -33,7 +33,7 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions /// public static AcBinarySerializerOptions FastMode => new() { - UseStringInterning = false, + UseStringInterning = StringInterningMode.None, ReferenceHandling = ReferenceHandlingMode.None }; @@ -44,7 +44,7 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions public static AcBinarySerializerOptions ShallowCopy => new() { MaxDepth = 0, - UseStringInterning = false, + UseStringInterning = StringInterningMode.None, ReferenceHandling = ReferenceHandlingMode.None }; @@ -89,12 +89,13 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions public bool UseMetadata { get; init; } = false; /// - /// Whether to intern repeated strings. - /// When enabled, duplicate strings are stored once and referenced by index. - /// Reduces size and memory for objects with many repeated string values. - /// Default: true + /// Controls how string interning is applied during serialization. + /// None: No interning, all strings written inline. + /// Attribute: Only properties with [AcStringIntern(true)] are interned. + /// All: All strings within length limits are interned (legacy behavior). + /// Default: All /// - public bool UseStringInterning { get; set; } = true; + public StringInterningMode UseStringInterning { get; set; } = StringInterningMode.All; /// /// Minimum string length to consider for interning. diff --git a/AyCode.Core/Serializers/Binaries/AcStringInternAttribute.cs b/AyCode.Core/Serializers/Binaries/AcStringInternAttribute.cs new file mode 100644 index 0000000..5d30d37 --- /dev/null +++ b/AyCode.Core/Serializers/Binaries/AcStringInternAttribute.cs @@ -0,0 +1,25 @@ +namespace AyCode.Core.Serializers.Binaries; + +/// +/// 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. +/// +[AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)] +public sealed class AcStringInternAttribute : Attribute +{ + /// + /// Whether string interning is enabled for this property. + /// + public bool Enabled { get; } + + /// + /// Creates a new AcStringInternAttribute. + /// + /// True to enable interning, false to disable. + public AcStringInternAttribute(bool enabled) + { + Enabled = enabled; + } +} diff --git a/AyCode.Core/Serializers/Binaries/StringInterningMode.cs b/AyCode.Core/Serializers/Binaries/StringInterningMode.cs new file mode 100644 index 0000000..f69e872 --- /dev/null +++ b/AyCode.Core/Serializers/Binaries/StringInterningMode.cs @@ -0,0 +1,24 @@ +namespace AyCode.Core.Serializers.Binaries; + +/// +/// Controls string interning behavior during binary serialization. +/// +public enum StringInterningMode +{ + /// + /// No string interning. All strings are written inline. + /// + None = 0, + + /// + /// Only intern strings on properties marked with [AcStringIntern(true)]. + /// Properties without attribute or with [AcStringIntern(false)] are written inline. + /// + Attribute = 1, + + /// + /// Intern all strings that meet the length criteria (MinStringInternLength to MaxStringInternLength). + /// [AcStringIntern(false)] can opt-out specific properties. + /// + All = 2 +}