From a221414e8dd75dd756a8a79b44bfa163935d82c9 Mon Sep 17 00:00:00 2001 From: RejectKid Date: Tue, 21 Jul 2026 16:02:54 -0400 Subject: [PATCH] Optimize generated scalar getters --- .../BitsKit.Benchmarks.Regression.csproj | 4 + .../BitsKitBenchmark.GeneratedAccessors.cs | 139 +++++++++++++++++- .../GeneratedAccessorBenchmarkModels.cs | 53 +++++++ BitsKit.Generator/Models/BitFieldModel.cs | 93 +++++++++++- BitsKit.Generator/Models/BooleanFieldModel.cs | 3 + BitsKit.Generator/Models/EnumFieldModel.cs | 3 + .../Models/IntegralFieldModel.cs | 8 + BitsKit.Generator/StringConstants.cs | 1 + BitsKit.Tests/GeneratorTests.Models.cs | 29 ++++ BitsKit.Tests/GeneratorTests.cs | 91 ++++++++---- CHANGELOG.md | 7 +- CONTRIBUTING.md | 4 +- README.md | 2 +- eng/Run-Benchmark-Regression.ps1 | 46 +++++- 14 files changed, 441 insertions(+), 42 deletions(-) diff --git a/BitsKit.Benchmarks.Regression/BitsKit.Benchmarks.Regression.csproj b/BitsKit.Benchmarks.Regression/BitsKit.Benchmarks.Regression.csproj index 1c9e2f6..df0d3f5 100644 --- a/BitsKit.Benchmarks.Regression/BitsKit.Benchmarks.Regression.csproj +++ b/BitsKit.Benchmarks.Regression/BitsKit.Benchmarks.Regression.csproj @@ -6,6 +6,7 @@ enable false $(MSBuildThisFileDirectory)..\BitsKit\BitsKit.csproj + $(MSBuildThisFileDirectory)..\BitsKit.Generator\BitsKit.Generator.csproj @@ -14,11 +15,13 @@ + + @@ -29,6 +32,7 @@ + diff --git a/BitsKit.Benchmarks/BitsKitBenchmark.GeneratedAccessors.cs b/BitsKit.Benchmarks/BitsKitBenchmark.GeneratedAccessors.cs index b17a705..9e4076d 100644 --- a/BitsKit.Benchmarks/BitsKitBenchmark.GeneratedAccessors.cs +++ b/BitsKit.Benchmarks/BitsKitBenchmark.GeneratedAccessors.cs @@ -10,10 +10,12 @@ public partial class BitsKitBenchmark private readonly GeneratedAccessorLsbModel[] _generatedAccessorGetModels = CreateGeneratedAccessorModels(); private readonly GeneratedAccessorLsbModel[] _generatedAccessorSetModels = CreateGeneratedAccessorModels(); + private readonly GeneratedAccessorMemoryModel[] _generatedAccessorMemoryModels = CreateGeneratedAccessorMemoryModels(); + private readonly GeneratedAccessorInlineArrayModel[] _generatedAccessorInlineArrayModels = CreateGeneratedAccessorInlineArrayModels(); [Benchmark(OperationsPerInvoke = AccessorOperations)] [BenchmarkCategory("GeneratedAccessor", "Integral", "Get", "LSB")] - public uint GeneratedAccessorGet() + public uint GeneratedAccessorGetUInt32LSB() { uint sum = 0; @@ -25,7 +27,7 @@ public uint GeneratedAccessorGet() [Benchmark(OperationsPerInvoke = AccessorOperations)] [BenchmarkCategory("GeneratedAccessor", "Integral", "Set", "LSB")] - public uint GeneratedAccessorSet() + public uint GeneratedAccessorSetUInt32LSB() { for (int i = 0; i < AccessorOperations; i++) _generatedAccessorSetModels[i & AccessorModelMask].Value = (uint)i; @@ -33,12 +35,143 @@ public uint GeneratedAccessorSet() return _generatedAccessorSetModels[0].BackingField; } + [Benchmark(OperationsPerInvoke = AccessorOperations)] + [BenchmarkCategory("GeneratedAccessor", "Integral", "Get", "Signed", "LSB")] + public int GeneratedAccessorGetInt32LSB() + { + int sum = 0; + + for (int i = 0; i < AccessorOperations; i++) + sum += _generatedAccessorGetModels[i & AccessorModelMask].SignedValue; + + return sum; + } + + [Benchmark(OperationsPerInvoke = AccessorOperations)] + [BenchmarkCategory("GeneratedAccessor", "Boolean", "Get", "LSB")] + public int GeneratedAccessorGetBooleanLSB() + { + int count = 0; + + for (int i = 0; i < AccessorOperations; i++) + count += _generatedAccessorGetModels[i & AccessorModelMask].Flag ? 1 : 0; + + return count; + } + + [Benchmark(OperationsPerInvoke = AccessorOperations)] + [BenchmarkCategory("GeneratedAccessor", "Enum", "Get", "LSB")] + public uint GeneratedAccessorGetEnumLSB() + { + uint sum = 0; + + for (int i = 0; i < AccessorOperations; i++) + sum += (uint)_generatedAccessorGetModels[i & AccessorModelMask].Kind; + + return sum; + } + + [Benchmark(OperationsPerInvoke = AccessorOperations)] + [BenchmarkCategory("GeneratedAccessor", "Integral", "Get", "UInt64", "LSB")] + public ulong GeneratedAccessorGetUInt64LSB() + { + ulong sum = 0; + + for (int i = 0; i < AccessorOperations; i++) + sum += _generatedAccessorGetModels[i & AccessorModelMask].WideValue; + + return sum; + } + + [Benchmark(OperationsPerInvoke = AccessorOperations)] + [BenchmarkCategory("GeneratedAccessor", "Integral", "Get", "MSB")] + public uint GeneratedAccessorGetUInt32MSB() + { + uint sum = 0; + + for (int i = 0; i < AccessorOperations; i++) + sum += _generatedAccessorGetModels[i & AccessorModelMask].MostSignificantValue; + + return sum; + } + + [Benchmark(OperationsPerInvoke = AccessorOperations)] + [BenchmarkCategory("GeneratedAccessor", "Memory", "Get", "LSB")] + public uint GeneratedAccessorGetMemoryLSB() + { + uint sum = 0; + + for (int i = 0; i < AccessorOperations; i++) + sum += _generatedAccessorMemoryModels[i & AccessorModelMask].Value; + + return sum; + } + + [Benchmark(OperationsPerInvoke = AccessorOperations)] + [BenchmarkCategory("GeneratedAccessor", "Memory", "Set", "LSB")] + public uint GeneratedAccessorSetMemoryLSB() + { + for (int i = 0; i < AccessorOperations; i++) + _generatedAccessorMemoryModels[i & AccessorModelMask].Value = (uint)i; + + return BitConverter.ToUInt32(_generatedAccessorMemoryModels[0].BackingField.Span); + } + + [Benchmark(OperationsPerInvoke = AccessorOperations)] + [BenchmarkCategory("GeneratedAccessor", "InlineArray", "Get", "LSB")] + public uint GeneratedAccessorGetInlineArrayLSB() + { + uint sum = 0; + + for (int i = 0; i < AccessorOperations; i++) + sum += _generatedAccessorInlineArrayModels[i & AccessorModelMask].Value; + + return sum; + } + private static GeneratedAccessorLsbModel[] CreateGeneratedAccessorModels() { var models = new GeneratedAccessorLsbModel[AccessorModelCount]; for (int i = 0; i < models.Length; i++) - models[i].BackingField = unchecked((uint)i * 0x9E3779B9u); + { + uint value = unchecked((uint)i * 0x9E3779B9u); + models[i].BackingField = value; + models[i].SignedBackingField = unchecked((int)(value ^ 0xA5A5A5A5u)); + models[i].BooleanBackingField = value; + models[i].EnumBackingField = value; + models[i].WideBackingField = ((ulong)value << 32) | ~value; + models[i].MostSignificantBackingField = value; + } + + return models; + } + + private static GeneratedAccessorMemoryModel[] CreateGeneratedAccessorMemoryModels() + { + var models = new GeneratedAccessorMemoryModel[AccessorModelCount]; + + for (int i = 0; i < models.Length; i++) + { + uint value = unchecked((uint)i * 0x9E3779B9u); + models[i].BackingField = BitConverter.GetBytes(value); + } + + return models; + } + + private static GeneratedAccessorInlineArrayModel[] CreateGeneratedAccessorInlineArrayModels() + { + var models = new GeneratedAccessorInlineArrayModel[AccessorModelCount]; + + for (int i = 0; i < models.Length; i++) + { + uint value = unchecked((uint)i * 0x9E3779B9u); + models[i][0] = (byte)value; + models[i][1] = (byte)(value >> 8); + models[i][2] = (byte)(value >> 16); + models[i][3] = (byte)(value >> 24); + } return models; } diff --git a/BitsKit.Benchmarks/GeneratedAccessorBenchmarkModels.cs b/BitsKit.Benchmarks/GeneratedAccessorBenchmarkModels.cs index a72948f..76e67b2 100644 --- a/BitsKit.Benchmarks/GeneratedAccessorBenchmarkModels.cs +++ b/BitsKit.Benchmarks/GeneratedAccessorBenchmarkModels.cs @@ -8,4 +8,57 @@ public partial struct GeneratedAccessorLsbModel [BitField(5)] [BitField("Value", 11)] public uint BackingField; + + [BitField(7)] + [BitField("SignedValue", 13)] + public int SignedBackingField; + + [BitField(5)] + [BooleanField("Flag")] + public uint BooleanBackingField; + + [BitField(3)] + [EnumField("Kind", 3, typeof(GeneratedAccessorKind))] + public uint EnumBackingField; + + [BitField(9)] + [BitField("WideValue", 43)] + public ulong WideBackingField; + + [BitField(5)] + [BitField("MostSignificantValue", 11, ReverseBitOrder = true)] + public uint MostSignificantBackingField; +} + +public enum GeneratedAccessorKind : uint +{ + Zero, + One, + Two, + Three, + Four, + Five, + Six, + Seven +} + +[BitObject(BitOrder.LeastSignificant)] +public partial struct GeneratedAccessorMemoryModel +{ + [BitField(5)] + [BitField("Value", 11, BitFieldType.UInt32)] + public Memory BackingField; } + +#if NET8_0_OR_GREATER + +[BitObject(BitOrder.LeastSignificant)] +[System.Runtime.CompilerServices.InlineArray(4)] +public partial struct GeneratedAccessorInlineArrayModel +{ + [BitField(5)] + [BitField("Value", 11, BitFieldType.UInt32, Modifiers = BitFieldModifiers.ReadOnly)] + private byte _element; +} + +#endif diff --git a/BitsKit.Generator/Models/BitFieldModel.cs b/BitsKit.Generator/Models/BitFieldModel.cs index ffaf855..562f545 100644 --- a/BitsKit.Generator/Models/BitFieldModel.cs +++ b/BitsKit.Generator/Models/BitFieldModel.cs @@ -181,6 +181,95 @@ BackingFieldType.Span or _ => throw new NotSupportedException() }; + /// + /// Creates a specialized scalar read for fixed-width integral backing fields. + /// + protected bool TryGetDirectIntegralReadExpression(out string expression) + { + expression = string.Empty; + + if (!TryGetDirectIntegralInfo( + out int workingWidth, + out string unsignedType)) + { + return false; + } + + string backingType = FieldType!.Value.ToString(); + string unsignedSource = $"unchecked(({unsignedType}){{4}})"; + if (BitOrder == BitOrder.MostSignificant) + { + string extracted = + $"(BinaryPrimitives.ReverseEndianness({unsignedSource}) << {BitOffset}) >> {workingWidth - BitCount}"; + + if (FieldType is BitFieldType.SByte or + BitFieldType.Int16 or + BitFieldType.Int32 or + BitFieldType.Int64) + { + string signedType = workingWidth == 64 ? "Int64" : "Int32"; + int signShift = workingWidth - BitCount; + expression = + $"unchecked(({backingType})((unchecked(({signedType})({extracted})) << {signShift}) >> {signShift}))"; + } + else + { + expression = $"unchecked(({backingType})({extracted}))"; + } + + return true; + } + + if (FieldType is BitFieldType.SByte or + BitFieldType.Int16 or + BitFieldType.Int32 or + BitFieldType.Int64) + { + string signedType = workingWidth == 64 ? "Int64" : "Int32"; + int leftShift = workingWidth - BitOffset - BitCount; + int rightShift = workingWidth - BitCount; + expression = + $"unchecked(({backingType})(unchecked(({signedType})({unsignedSource} << {leftShift})) >> {rightShift}))"; + } + else + { + ulong valueMask = BitCount == 64 ? ulong.MaxValue : (1UL << BitCount) - 1; + string mask = FormatMask(valueMask, workingWidth); + expression = + $"unchecked(({backingType})(({unsignedSource} >> {BitOffset}) & {mask}))"; + } + + return true; + } + + /// + /// Creates a specialized scalar bit test for integral backing fields. + /// + protected bool TryGetDirectIntegralBooleanReadExpression(out string expression) + { + expression = string.Empty; + + if (!TryGetDirectIntegralInfo( + out int workingWidth, + out string unsignedType)) + { + return false; + } + + if (BitOrder == BitOrder.MostSignificant) + { + expression = + $"((BinaryPrimitives.ReverseEndianness(unchecked(({unsignedType}){{4}})) << {BitOffset}) >> {workingWidth - 1}) != 0"; + } + else + { + string mask = FormatMask(1UL << BitOffset, workingWidth); + expression = $"(unchecked(({unsignedType}){{4}}) & {mask}) != 0"; + } + + return true; + } + /// /// Creates a specialized scalar assignment for fixed-width integral backing fields. /// @@ -190,7 +279,8 @@ protected bool TryGetDirectIntegralWriteExpression(string valueExpression, out s if (!TryGetDirectIntegralInfo( out int workingWidth, - out string unsignedType)) + out string unsignedType) || + BitOrder != BitOrder.LeastSignificant) { return false; } @@ -223,7 +313,6 @@ private bool TryGetDirectIntegralInfo( unsignedType = workingWidth == 64 ? "UInt64" : "UInt32"; return BackingFieldType == BackingFieldType.Integral && - BitOrder == BitOrder.LeastSignificant && backingWidth != 0 && BitCount > 0 && BitOffset >= 0 && diff --git a/BitsKit.Generator/Models/BooleanFieldModel.cs b/BitsKit.Generator/Models/BooleanFieldModel.cs index ac6b23d..8ca232e 100644 --- a/BitsKit.Generator/Models/BooleanFieldModel.cs +++ b/BitsKit.Generator/Models/BooleanFieldModel.cs @@ -30,6 +30,9 @@ public BooleanFieldModel(AttributeData attributeData, TypeSymbolProcessor? typeS protected override string GetGetterTemplate() { + if (TryGetDirectIntegralBooleanReadExpression(out string expression)) + return "{0} {1} => " + expression + ";"; + string template = BackingFieldType == BackingFieldType.Integral ? StringConstants.BooleanGetterTemplate : StringConstants.BooleanSpanGetterTemplate; diff --git a/BitsKit.Generator/Models/EnumFieldModel.cs b/BitsKit.Generator/Models/EnumFieldModel.cs index f35c08e..ea9fbc1 100644 --- a/BitsKit.Generator/Models/EnumFieldModel.cs +++ b/BitsKit.Generator/Models/EnumFieldModel.cs @@ -50,6 +50,9 @@ public EnumFieldModel(AttributeData attributeData, TypeSymbolProcessor? typeSymb protected override string GetGetterTemplate() { + if (TryGetDirectIntegralReadExpression(out string expression)) + return $"{{0}} {{1}} => ({ReturnType})({expression});"; + return string.Format(StringConstants.ExplicitGetterTemplate, GetterSource(), ReturnType); } diff --git a/BitsKit.Generator/Models/IntegralFieldModel.cs b/BitsKit.Generator/Models/IntegralFieldModel.cs index 0705d49..0606e54 100644 --- a/BitsKit.Generator/Models/IntegralFieldModel.cs +++ b/BitsKit.Generator/Models/IntegralFieldModel.cs @@ -42,6 +42,14 @@ public IntegralFieldModel(AttributeData attributeData, TypeSymbolProcessor? type protected override string GetGetterTemplate() { + if (TryGetDirectIntegralReadExpression(out string expression)) + { + if (IsTypeCast) + expression = $"({ReturnType})({expression})"; + + return "{0} {1} => " + expression + ";"; + } + if (IsTypeCast) return string.Format(StringConstants.ExplicitGetterTemplate, GetterSource(), ReturnType); diff --git a/BitsKit.Generator/StringConstants.cs b/BitsKit.Generator/StringConstants.cs index 5836b77..3255dc3 100644 --- a/BitsKit.Generator/StringConstants.cs +++ b/BitsKit.Generator/StringConstants.cs @@ -23,6 +23,7 @@ internal static class StringConstants #pragma warning disable IDE0161 // Convert to file-scoped namespace using System; + using System.Buffers.Binary; using System.Runtime.InteropServices; using BitsKit.Primitives; diff --git a/BitsKit.Tests/GeneratorTests.Models.cs b/BitsKit.Tests/GeneratorTests.Models.cs index 2cafa3e..6d73f88 100644 --- a/BitsKit.Tests/GeneratorTests.Models.cs +++ b/BitsKit.Tests/GeneratorTests.Models.cs @@ -213,7 +213,36 @@ public partial struct OptimizedIntegralAccessorStruct [BooleanField("Flag")] public uint BooleanBacking; + [BitField(5)] + [BooleanField("SignedFlag")] + public int SignedBooleanBacking; + + [BitField(5)] + [BooleanField("ReversedSignedFlag", ReverseBitOrder = true)] + public int ReversedSignedBooleanBacking; + [BitField(5)] [EnumField("EnumValue", 2, typeof(TestEnum))] public uint EnumBacking; } + +[BitObject(BitOrder.LeastSignificant)] +public partial struct OptimizedMemoryAccessorStruct +{ + [BitField(5)] + [BitField("Value", 11, BitFieldType.UInt32)] + public Memory Backing; +} + +#if NET8_0_OR_GREATER + +[BitObject(BitOrder.LeastSignificant)] +[InlineArray(4)] +public partial struct OptimizedInlineArrayAccessorStruct +{ + [BitField(5)] + [BitField("Value", 11, BitFieldType.UInt32, Modifiers = BitFieldModifiers.ReadOnly)] + private byte _element; +} + +#endif diff --git a/BitsKit.Tests/GeneratorTests.cs b/BitsKit.Tests/GeneratorTests.cs index 3bf8b66..2e235aa 100644 --- a/BitsKit.Tests/GeneratorTests.cs +++ b/BitsKit.Tests/GeneratorTests.cs @@ -240,6 +240,8 @@ public void OptimizedIntegralAccessorsMatchBitPrimitives() UInt64Backing = ((ulong)(uint)random.Next() << 32) | (uint)random.Next(), ReversedUInt64Backing = ((ulong)(uint)random.Next() << 32) | (uint)random.Next(), BooleanBacking = (uint)random.Next(), + SignedBooleanBacking = random.Next(), + ReversedSignedBooleanBacking = random.Next(), EnumBacking = (uint)random.Next() }; @@ -250,6 +252,8 @@ public void OptimizedIntegralAccessorsMatchBitPrimitives() Assert.AreEqual(BitPrimitives.ReadUInt64LSB(actual.UInt64Backing, 0, 64), actual.FullUInt64Value); Assert.AreEqual(BitPrimitives.ReadUInt64MSB(actual.ReversedUInt64Backing, 7, 43), actual.ReversedUInt64Value); Assert.AreEqual(BitPrimitives.ReadUInt32LSB(actual.BooleanBacking, 5, 1) == 1, actual.Flag); + Assert.AreEqual((actual.SignedBooleanBacking & (1 << 5)) != 0, actual.SignedFlag); + Assert.AreEqual(BitPrimitives.ReadInt32MSB(actual.ReversedSignedBooleanBacking, 5, 1) != 0, actual.ReversedSignedFlag); Assert.AreEqual((TestEnum)BitPrimitives.ReadUInt32LSB(actual.EnumBacking, 5, 2), actual.EnumValue); byte byteValue = (byte)random.Next(); @@ -292,6 +296,16 @@ public void OptimizedIntegralAccessorsMatchBitPrimitives() actual.Flag = flag; Assert.AreEqual(expectedBoolean, actual.BooleanBacking); + int expectedSignedBoolean = actual.SignedBooleanBacking; + BitPrimitives.WriteInt32LSB(ref expectedSignedBoolean, 5, flag ? 1 : 0, 1); + actual.SignedFlag = flag; + Assert.AreEqual(expectedSignedBoolean, actual.SignedBooleanBacking); + + int expectedReversedSignedBoolean = actual.ReversedSignedBooleanBacking; + BitPrimitives.WriteInt32MSB(ref expectedReversedSignedBoolean, 5, flag ? 1 : 0, 1); + actual.ReversedSignedFlag = flag; + Assert.AreEqual(expectedReversedSignedBoolean, actual.ReversedSignedBooleanBacking); + TestEnum enumValue = (TestEnum)random.Next(4); uint expectedEnum = actual.EnumBacking; BitPrimitives.WriteUInt32LSB(ref expectedEnum, 5, (uint)enumValue, 2); @@ -300,6 +314,33 @@ public void OptimizedIntegralAccessorsMatchBitPrimitives() } } + [TestMethod] + public void OptimizedStorageGettersMatchBitPrimitives() + { + Random random = new(0x51A6E); + + for (int i = 0; i < 1000; i++) + { + byte[] bytes = new byte[4]; + random.NextBytes(bytes); + var memory = new OptimizedMemoryAccessorStruct { Backing = bytes }; + + Assert.AreEqual(BitPrimitives.ReadUInt32LSB(bytes, 5, 11), memory.Value); + + uint nextValue = (uint)random.Next(1 << 11); + byte[] expectedBytes = (byte[])bytes.Clone(); + BitPrimitives.WriteUInt32LSB(expectedBytes, 5, nextValue, 11); + memory.Value = nextValue; + CollectionAssert.AreEqual(expectedBytes, bytes); + +#if NET8_0_OR_GREATER + var inline = new OptimizedInlineArrayAccessorStruct(); + bytes.CopyTo((Span)inline); + Assert.AreEqual(BitPrimitives.ReadUInt32LSB(bytes, 5, 11), inline.Value); +#endif + } + } + [TestMethod] public void ReadOnlyMemberTest() { @@ -323,7 +364,7 @@ partial struct BitFieldReadOnly { public Int32 Generated00 { - get => BitPrimitives.ReadInt32LSB(BackingField00, 0, 2); + get => unchecked((Int32)(unchecked((Int32)(unchecked((UInt32)BackingField00) << 30)) >> 30)); } public Int32 Generated10 @@ -366,7 +407,7 @@ partial struct BitFieldReadOnly { public Int32 Generated00 { - get => BitPrimitives.ReadInt32LSB(BackingField00, 0, 2); + get => unchecked((Int32)(unchecked((Int32)(unchecked((UInt32)BackingField00) << 30)) >> 30)); } public Int32 Generated10 @@ -425,54 +466,54 @@ partial class BitFieldGeneratorTest { public Int32 Generated01 { - get => BitPrimitives.ReadInt32LSB(BackingField00, 0, 2); + get => unchecked((Int32)(unchecked((Int32)(unchecked((UInt32)BackingField00) << 30)) >> 30)); set => BackingField00 = unchecked((Int32)((unchecked((UInt32)BackingField00) & ~0x3U) | ((unchecked((UInt32)(value)) << 0) & 0x3U))); } private Int32 Generated02 { - get => BitPrimitives.ReadInt32LSB(BackingField00, 4, 2); + get => unchecked((Int32)(unchecked((Int32)(unchecked((UInt32)BackingField00) << 26)) >> 30)); set => BackingField00 = unchecked((Int32)((unchecked((UInt32)BackingField00) & ~0x30U) | ((unchecked((UInt32)(value)) << 4) & 0x30U))); } internal Int32 Generated03 { - get => BitPrimitives.ReadInt32LSB(BackingField00, 6, 2); + get => unchecked((Int32)(unchecked((Int32)(unchecked((UInt32)BackingField00) << 24)) >> 30)); set => BackingField00 = unchecked((Int32)((unchecked((UInt32)BackingField00) & ~0xC0U) | ((unchecked((UInt32)(value)) << 6) & 0xC0U))); } public Int32 Generated04 { - get => BitPrimitives.ReadInt32LSB(BackingField00, 8, 2); + get => unchecked((Int32)(unchecked((Int32)(unchecked((UInt32)BackingField00) << 22)) >> 30)); } public Int32 Generated05 { - get => BitPrimitives.ReadInt32LSB(BackingField00, 10, 2); + get => unchecked((Int32)(unchecked((Int32)(unchecked((UInt32)BackingField00) << 20)) >> 30)); init => BackingField00 = unchecked((Int32)((unchecked((UInt32)BackingField00) & ~0xC00U) | ((unchecked((UInt32)(value)) << 10) & 0xC00U))); } public Int32 Generated06 { - get => BitPrimitives.ReadInt32MSB(BackingField00, 12, 2); + get => unchecked((Int32)((unchecked((Int32)((BinaryPrimitives.ReverseEndianness(unchecked((UInt32)BackingField00)) << 12) >> 30)) << 30) >> 30)); set => BitPrimitives.WriteInt32MSB(ref BackingField00, 12, value, 2); } protected Int32 Generated07 { - get => BitPrimitives.ReadInt32LSB(BackingField00, 14, 2); + get => unchecked((Int32)(unchecked((Int32)(unchecked((UInt32)BackingField00) << 16)) >> 30)); set => BackingField00 = unchecked((Int32)((unchecked((UInt32)BackingField00) & ~0xC000U) | ((unchecked((UInt32)(value)) << 14) & 0xC000U))); } protected internal Int32 Generated08 { - get => BitPrimitives.ReadInt32LSB(BackingField00, 16, 2); + get => unchecked((Int32)(unchecked((Int32)(unchecked((UInt32)BackingField00) << 14)) >> 30)); set => BackingField00 = unchecked((Int32)((unchecked((UInt32)BackingField00) & ~0x30000U) | ((unchecked((UInt32)(value)) << 16) & 0x30000U))); } private protected Int32 Generated09 { - get => BitPrimitives.ReadInt32LSB(BackingField00, 18, 2); + get => unchecked((Int32)(unchecked((Int32)(unchecked((UInt32)BackingField00) << 12)) >> 30)); set => BackingField00 = unchecked((Int32)((unchecked((UInt32)BackingField00) & ~0xC0000U) | ((unchecked((UInt32)(value)) << 18) & 0xC0000U))); } @@ -571,60 +612,60 @@ partial class BitFieldGeneratorTest { public Int32 Generated01 { - get => BitPrimitives.ReadInt32LSB(BackingField00, 0, 2); + get => unchecked((Int32)(unchecked((Int32)(unchecked((UInt32)BackingField00) << 30)) >> 30)); set => BackingField00 = unchecked((Int32)((unchecked((UInt32)BackingField00) & ~0x3U) | ((unchecked((UInt32)(value)) << 0) & 0x3U))); } private Int32 Generated02 { - get => BitPrimitives.ReadInt32LSB(BackingField00, 4, 2); + get => unchecked((Int32)(unchecked((Int32)(unchecked((UInt32)BackingField00) << 26)) >> 30)); set => BackingField00 = unchecked((Int32)((unchecked((UInt32)BackingField00) & ~0x30U) | ((unchecked((UInt32)(value)) << 4) & 0x30U))); } internal Int32 Generated03 { - get => BitPrimitives.ReadInt32LSB(BackingField00, 6, 2); + get => unchecked((Int32)(unchecked((Int32)(unchecked((UInt32)BackingField00) << 24)) >> 30)); set => BackingField00 = unchecked((Int32)((unchecked((UInt32)BackingField00) & ~0xC0U) | ((unchecked((UInt32)(value)) << 6) & 0xC0U))); } public Int32 Generated04 { - get => BitPrimitives.ReadInt32LSB(BackingField00, 8, 2); + get => unchecked((Int32)(unchecked((Int32)(unchecked((UInt32)BackingField00) << 22)) >> 30)); } public Int32 Generated05 { - get => BitPrimitives.ReadInt32LSB(BackingField00, 10, 2); + get => unchecked((Int32)(unchecked((Int32)(unchecked((UInt32)BackingField00) << 20)) >> 30)); init => BackingField00 = unchecked((Int32)((unchecked((UInt32)BackingField00) & ~0xC00U) | ((unchecked((UInt32)(value)) << 10) & 0xC00U))); } public Int32 Generated06 { - get => BitPrimitives.ReadInt32MSB(BackingField00, 12, 2); + get => unchecked((Int32)((unchecked((Int32)((BinaryPrimitives.ReverseEndianness(unchecked((UInt32)BackingField00)) << 12) >> 30)) << 30) >> 30)); set => BitPrimitives.WriteInt32MSB(ref BackingField00, 12, value, 2); } public required Int32 Generated07 { - get => BitPrimitives.ReadInt32LSB(BackingField00, 14, 2); + get => unchecked((Int32)(unchecked((Int32)(unchecked((UInt32)BackingField00) << 16)) >> 30)); set => BackingField00 = unchecked((Int32)((unchecked((UInt32)BackingField00) & ~0xC000U) | ((unchecked((UInt32)(value)) << 14) & 0xC000U))); } protected Int32 Generated08 { - get => BitPrimitives.ReadInt32LSB(BackingField00, 16, 2); + get => unchecked((Int32)(unchecked((Int32)(unchecked((UInt32)BackingField00) << 14)) >> 30)); set => BackingField00 = unchecked((Int32)((unchecked((UInt32)BackingField00) & ~0x30000U) | ((unchecked((UInt32)(value)) << 16) & 0x30000U))); } protected internal Int32 Generated09 { - get => BitPrimitives.ReadInt32LSB(BackingField00, 18, 2); + get => unchecked((Int32)(unchecked((Int32)(unchecked((UInt32)BackingField00) << 12)) >> 30)); set => BackingField00 = unchecked((Int32)((unchecked((UInt32)BackingField00) & ~0xC0000U) | ((unchecked((UInt32)(value)) << 18) & 0xC0000U))); } private protected Int32 Generated0A { - get => BitPrimitives.ReadInt32LSB(BackingField00, 20, 2); + get => unchecked((Int32)(unchecked((Int32)(unchecked((UInt32)BackingField00) << 10)) >> 30)); set => BackingField00 = unchecked((Int32)((unchecked((UInt32)BackingField00) & ~0x300000U) | ((unchecked((UInt32)(value)) << 20) & 0x300000U))); } } @@ -662,7 +703,7 @@ partial struct BooleanGeneratorTest { public System.Boolean Generated01 { - readonly get => BitPrimitives.ReadInt32LSB(BackingField00, 0, 1) == 1; + readonly get => (unchecked((UInt32)BackingField00) & 0x1U) != 0; set => BackingField00 = unchecked((Int32)((unchecked((UInt32)BackingField00) & ~0x1U) | ((unchecked((UInt32)(value ? 1 : 0)) << 0) & 0x1U))); } @@ -720,13 +761,13 @@ partial struct EnumGeneratorTest { public BitsKit.Tests.TestEnum Generated00 { - readonly get => (BitsKit.Tests.TestEnum)BitPrimitives.ReadInt32LSB(BackingField00, 0, 2); + readonly get => (BitsKit.Tests.TestEnum)(unchecked((Int32)(unchecked((Int32)(unchecked((UInt32)BackingField00) << 30)) >> 30))); set => BackingField00 = unchecked((Int32)((unchecked((UInt32)BackingField00) & ~0x3U) | ((unchecked((UInt32)(value)) << 0) & 0x3U))); } public BitsKit.Tests.TestEnum Generated01 { - readonly get => (BitsKit.Tests.TestEnum)BitPrimitives.ReadInt32LSB(BackingField00, 2, 2); + readonly get => (BitsKit.Tests.TestEnum)(unchecked((Int32)(unchecked((Int32)(unchecked((UInt32)BackingField00) << 28)) >> 30))); set => BackingField00 = unchecked((Int32)((unchecked((UInt32)BackingField00) & ~0xCU) | ((unchecked((UInt32)(value)) << 2) & 0xCU))); } @@ -791,7 +832,7 @@ partial struct BitFieldIntegerConversion { public Byte Generated00 { - readonly get => (Byte)BitPrimitives.ReadInt32LSB(BackingField00, 0, 2); + readonly get => (Byte)(unchecked((Int32)(unchecked((Int32)(unchecked((UInt32)BackingField00) << 30)) >> 30))); set => BackingField00 = unchecked((Int32)((unchecked((UInt32)BackingField00) & ~0x3U) | ((unchecked((UInt32)(value)) << 0) & 0x3U))); } diff --git a/CHANGELOG.md b/CHANGELOG.md index f12f51e..254e6da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,15 +7,18 @@ Notable changes to the community-maintained fork are documented here. This proje ### Added - `BitStreamWriter` supports sequential output to non-seekable streams such as compression, encryption, and network streams. -- An on-demand performance regression workflow compares the 56 operations shared with the original BitsKit fork point and can enforce a configurable slowdown tolerance. +- An on-demand performance regression workflow compares runtime operations with the original BitsKit fork point, including equivalent models compiled by each revision's source generator, and can enforce a configurable slowdown tolerance. ### Changed -- The source generator emits direct mask-and-shift setters for valid fixed-width, least-significant scalar fields, while retaining `BitPrimitives` for getters and for memory-backed, native-integer, reversed-order, and invalid-range cases. +- Integral-backed LSB and MSB generated getters now emit specialized mask-and-shift expressions instead of calling the general-purpose bit primitives. +- The source generator emits direct mask-and-shift setters for valid fixed-width, least-significant scalar fields, while retaining `BitPrimitives` for memory-backed, native-integer, reversed-order, and invalid-range cases. - Sequential `BitStreamReader` and `BitStreamWriter` single-bit operations use dedicated buffered fast paths while preserving existing seeking, EOF, and non-seekable-stream behavior. ### Fixed +- Boolean fields backed by signed integral fields now read a set bit correctly. + - Benchmark reports normalize batched measurements to a single library operation and include generated scalar accessors in the default feature suite. ## 1.4.0 - 2026-07-20 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 42d7b54..7947314 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,7 +22,7 @@ For performance work, run a focused benchmark of the affected feature: Attach the generated report and environment metadata to the pull request. Use stable local hardware for performance-regression claims. -For optimization work, compare all 56 operations shared with the original repository on the same machine: +For optimization work, compare the runtime operations shared with the original repository plus equivalent models compiled by each revision's source generator on the same machine: ```powershell ./eng/Run-Benchmark-Regression.ps1 -Job Medium -RegressionTolerancePercent 5 @@ -30,6 +30,8 @@ For optimization work, compare all 56 operations shared with the original reposi The script builds both libraries for `net8.0`, runs them sequentially under the same .NET 10 BenchmarkDotNet host, and writes Markdown and JSON ratio reports under `artifacts/benchmark-regression`. Add `-Enforce` to return a failure when any operation exceeds the allowed slowdown. The **Original performance regression** workflow exposes the same settings for on-demand hosted runs; use `Dry` only to validate the harness, not for performance conclusions. +During optimization, use `-MethodFilter 'GeneratedAccessor*'` (or another PowerShell wildcard) to run a focused comparison before the complete confirmation pass. + ## Pull requests - Add tests for fixes and new behavior. diff --git a/README.md b/README.md index 4c5313f..ce17165 100644 --- a/README.md +++ b/README.md @@ -248,7 +248,7 @@ static int Decode(uint value) ## Benchmarks -The [benchmark workflow](https://github.com/RejectKid/BitsKit/actions/workflows/benchmarks.yml) measures the library's features on .NET 10 every week and on demand. The report covers LSB/MSB bit primitives, generated scalar accessors, and the array-, span-, and stream-backed readers and writers across supported bit widths. Relevant pull requests run one focused dry benchmark to validate the harness. Each run includes a readable, categorized results table in its workflow summary and downloadable Markdown, JSON, logs, and environment metadata for 90 days. +The [benchmark workflow](https://github.com/RejectKid/BitsKit/actions/workflows/benchmarks.yml) measures the library's features on .NET 10 every week and on demand. The report covers LSB/MSB bit primitives, generated scalar, memory, and inline-array accessors, and the array-, span-, and stream-backed readers and writers across supported bit widths. Relevant pull requests run one focused dry benchmark to validate the harness. Each run includes a readable, categorized results table in its workflow summary and downloadable Markdown, JSON, logs, and environment metadata for 90 days. The published `Mean` values are normalized to one library operation, even though each benchmark processes a larger batch internally for measurement stability. This keeps primitive reads and writes, generated accessors, and the reader/writer types on the same nanoseconds-per-operation scale. Use stable local hardware and attach its generated report when making a performance-regression claim. diff --git a/eng/Run-Benchmark-Regression.ps1 b/eng/Run-Benchmark-Regression.ps1 index 206ab96..09ecb26 100644 --- a/eng/Run-Benchmark-Regression.ps1 +++ b/eng/Run-Benchmark-Regression.ps1 @@ -8,6 +8,8 @@ param( [ValidateRange(0, 100)] [double] $RegressionTolerancePercent = 5, + [string[]] $MethodFilter = @('*'), + [string] $ArtifactsPath = 'artifacts/benchmark-regression', [switch] $Enforce @@ -55,7 +57,7 @@ function Invoke-Tool { } } -function Get-SharedBenchmarkMethods { +function Get-ComparableBenchmarkMethods { $methods = [Collections.Generic.List[string]]::new() foreach ($operation in 'Read', 'Write') { @@ -73,8 +75,22 @@ function Get-SharedBenchmarkMethods { } } - if ($methods.Count -ne 56) { - throw "Expected 56 fork-point benchmark methods, found $($methods.Count)." + foreach ($method in + 'GeneratedAccessorGetUInt32LSB', + 'GeneratedAccessorSetUInt32LSB', + 'GeneratedAccessorGetInt32LSB', + 'GeneratedAccessorGetBooleanLSB', + 'GeneratedAccessorGetEnumLSB', + 'GeneratedAccessorGetUInt64LSB', + 'GeneratedAccessorGetUInt32MSB', + 'GeneratedAccessorGetMemoryLSB', + 'GeneratedAccessorSetMemoryLSB', + 'GeneratedAccessorGetInlineArrayLSB') { + $methods.Add($method) + } + + if ($methods.Count -ne 66) { + throw "Expected 66 comparable benchmark methods, found $($methods.Count)." } return $methods @@ -88,13 +104,17 @@ function Invoke-BenchmarkVariant { [Parameter(Mandatory)] [string] $BitsKitProjectPath, + [Parameter(Mandatory)] + [string] $BitsKitGeneratorProjectPath, + [Parameter(Mandatory)] [string[]] $Methods ) $variantArtifacts = Join-Path $runArtifactsPath $Name $buildProperties = @( - "-p:BitsKitProjectPath=$BitsKitProjectPath" + "-p:BitsKitProjectPath=$BitsKitProjectPath", + "-p:BitsKitGeneratorProjectPath=$BitsKitGeneratorProjectPath" ) $cleanArguments = @( 'clean', @@ -178,15 +198,24 @@ try { Invoke-Tool -FileName 'git' -Arguments @('worktree', 'add', '--detach', $baselineRoot, $BaselineCommit) $worktreeCreated = $true - $methods = @(Get-SharedBenchmarkMethods) + $allMethods = @(Get-ComparableBenchmarkMethods) + $methods = @($allMethods | Where-Object { + $method = $_ + @($MethodFilter | Where-Object { $method -like $_ }).Count -ne 0 + }) + if ($methods.Count -eq 0) { + throw "No benchmark methods matched: $($MethodFilter -join ', ')." + } $baselineProjectPath = Join-Path $baselineRoot 'BitsKit/BitsKit.csproj' + $baselineGeneratorProjectPath = Join-Path $baselineRoot 'BitsKit.Generator/BitsKit.Generator.csproj' $currentProjectPath = Join-Path $repositoryRoot 'BitsKit/BitsKit.csproj' + $currentGeneratorProjectPath = Join-Path $repositoryRoot 'BitsKit.Generator/BitsKit.Generator.csproj' Write-Host "Running fork-point benchmarks from $BaselineCommit..." - $baseline = Invoke-BenchmarkVariant -Name 'baseline' -BitsKitProjectPath $baselineProjectPath -Methods $methods + $baseline = Invoke-BenchmarkVariant -Name 'baseline' -BitsKitProjectPath $baselineProjectPath -BitsKitGeneratorProjectPath $baselineGeneratorProjectPath -Methods $methods Write-Host 'Running current benchmarks...' - $current = Invoke-BenchmarkVariant -Name 'current' -BitsKitProjectPath $currentProjectPath -Methods $methods + $current = Invoke-BenchmarkVariant -Name 'current' -BitsKitProjectPath $currentProjectPath -BitsKitGeneratorProjectPath $currentGeneratorProjectPath -Methods $methods $baselineByMethod = @{} foreach ($benchmark in $baseline.Benchmarks) { @@ -274,7 +303,8 @@ try { $report.Add("- Job: ``$Job``") $report.Add("- Host: $($baseline.HostEnvironmentInfo.ProcessorName), $($baseline.HostEnvironmentInfo.RuntimeVersion)") $report.Add("- Allowed regression: $($RegressionTolerancePercent.ToString('0.##'))%") - $report.Add("- Shared operations: $($comparison.Count)") + $report.Add("- Method filter: $($MethodFilter -join ', ')") + $report.Add("- Comparable operations: $($comparison.Count)") $report.Add("- Geometric-mean change: $($geometricMeanChange.ToString('+0.00;-0.00;0.00'))%") $report.Add("- Regressions beyond tolerance: $($regressions.Count)") $report.Add("- Inconclusive beyond-mean-threshold results: $($inconclusive.Count)")