From 941111a5091e3be12b12bbab03029985a34f29fc Mon Sep 17 00:00:00 2001 From: RejectKid Date: Tue, 21 Jul 2026 17:11:27 -0400 Subject: [PATCH] Optimize generated MSB scalar setters --- .../BitsKitBenchmark.GeneratedAccessors.cs | 43 ++++++++++++++++ .../GeneratedAccessorBenchmarkModels.cs | 12 +++++ BitsKit.Generator/Models/BitFieldModel.cs | 48 +++++++++++++++--- BitsKit.Tests/GeneratorTests.Models.cs | 22 ++++++++ BitsKit.Tests/GeneratorTests.cs | 50 +++++++++++++++++-- CHANGELOG.md | 2 +- README.md | 2 +- eng/Run-Benchmark-Regression.ps1 | 8 ++- 8 files changed, 172 insertions(+), 15 deletions(-) diff --git a/BitsKit.Benchmarks/BitsKitBenchmark.GeneratedAccessors.cs b/BitsKit.Benchmarks/BitsKitBenchmark.GeneratedAccessors.cs index 9e4076d..b1eb82c 100644 --- a/BitsKit.Benchmarks/BitsKitBenchmark.GeneratedAccessors.cs +++ b/BitsKit.Benchmarks/BitsKitBenchmark.GeneratedAccessors.cs @@ -95,6 +95,46 @@ public uint GeneratedAccessorGetUInt32MSB() return sum; } + [Benchmark(OperationsPerInvoke = AccessorOperations)] + [BenchmarkCategory("GeneratedAccessor", "Integral", "Set", "MSB")] + public uint GeneratedAccessorSetUInt32MSB() + { + for (int i = 0; i < AccessorOperations; i++) + _generatedAccessorSetModels[i & AccessorModelMask].MostSignificantValue = (uint)i; + + return _generatedAccessorSetModels[0].MostSignificantBackingField; + } + + [Benchmark(OperationsPerInvoke = AccessorOperations)] + [BenchmarkCategory("GeneratedAccessor", "Integral", "Set", "UInt64", "MSB")] + public ulong GeneratedAccessorSetUInt64MSB() + { + for (int i = 0; i < AccessorOperations; i++) + _generatedAccessorSetModels[i & AccessorModelMask].MostSignificantWideValue = (ulong)i; + + return _generatedAccessorSetModels[0].MostSignificantWideBackingField; + } + + [Benchmark(OperationsPerInvoke = AccessorOperations)] + [BenchmarkCategory("GeneratedAccessor", "Boolean", "Set", "MSB")] + public uint GeneratedAccessorSetBooleanMSB() + { + for (int i = 0; i < AccessorOperations; i++) + _generatedAccessorSetModels[i & AccessorModelMask].MostSignificantFlag = (i & 1) != 0; + + return _generatedAccessorSetModels[0].MostSignificantBooleanBackingField; + } + + [Benchmark(OperationsPerInvoke = AccessorOperations)] + [BenchmarkCategory("GeneratedAccessor", "Enum", "Set", "MSB")] + public uint GeneratedAccessorSetEnumMSB() + { + for (int i = 0; i < AccessorOperations; i++) + _generatedAccessorSetModels[i & AccessorModelMask].MostSignificantKind = (GeneratedAccessorKind)(i & 7); + + return _generatedAccessorSetModels[0].MostSignificantEnumBackingField; + } + [Benchmark(OperationsPerInvoke = AccessorOperations)] [BenchmarkCategory("GeneratedAccessor", "Memory", "Get", "LSB")] public uint GeneratedAccessorGetMemoryLSB() @@ -142,6 +182,9 @@ private static GeneratedAccessorLsbModel[] CreateGeneratedAccessorModels() models[i].EnumBackingField = value; models[i].WideBackingField = ((ulong)value << 32) | ~value; models[i].MostSignificantBackingField = value; + models[i].MostSignificantWideBackingField = ((ulong)value << 32) | ~value; + models[i].MostSignificantBooleanBackingField = value; + models[i].MostSignificantEnumBackingField = value; } return models; diff --git a/BitsKit.Benchmarks/GeneratedAccessorBenchmarkModels.cs b/BitsKit.Benchmarks/GeneratedAccessorBenchmarkModels.cs index 76e67b2..20de6c5 100644 --- a/BitsKit.Benchmarks/GeneratedAccessorBenchmarkModels.cs +++ b/BitsKit.Benchmarks/GeneratedAccessorBenchmarkModels.cs @@ -28,6 +28,18 @@ public partial struct GeneratedAccessorLsbModel [BitField(5)] [BitField("MostSignificantValue", 11, ReverseBitOrder = true)] public uint MostSignificantBackingField; + + [BitField(7)] + [BitField("MostSignificantWideValue", 43, ReverseBitOrder = true)] + public ulong MostSignificantWideBackingField; + + [BitField(5)] + [BooleanField("MostSignificantFlag", ReverseBitOrder = true)] + public uint MostSignificantBooleanBackingField; + + [BitField(3)] + [EnumField("MostSignificantKind", 3, typeof(GeneratedAccessorKind), ReverseBitOrder = true)] + public uint MostSignificantEnumBackingField; } public enum GeneratedAccessorKind : uint diff --git a/BitsKit.Generator/Models/BitFieldModel.cs b/BitsKit.Generator/Models/BitFieldModel.cs index 562f545..75e14e9 100644 --- a/BitsKit.Generator/Models/BitFieldModel.cs +++ b/BitsKit.Generator/Models/BitFieldModel.cs @@ -189,6 +189,7 @@ protected bool TryGetDirectIntegralReadExpression(out string expression) expression = string.Empty; if (!TryGetDirectIntegralInfo( + out _, out int workingWidth, out string unsignedType)) { @@ -250,6 +251,7 @@ protected bool TryGetDirectIntegralBooleanReadExpression(out string expression) expression = string.Empty; if (!TryGetDirectIntegralInfo( + out _, out int workingWidth, out string unsignedType)) { @@ -278,30 +280,60 @@ protected bool TryGetDirectIntegralWriteExpression(string valueExpression, out s expression = string.Empty; if (!TryGetDirectIntegralInfo( + out int backingWidth, out int workingWidth, - out string unsignedType) || - BitOrder != BitOrder.LeastSignificant) + out string unsignedType)) { return false; } - int shift = BitOffset; ulong valueMask = BitCount == 64 ? ulong.MaxValue : (1UL << BitCount) - 1; - ulong shiftedMask = valueMask << shift; - string fieldMask = FormatMask(shiftedMask, workingWidth); + string valueMaskLiteral = FormatMask(valueMask, workingWidth); string backingType = FieldType!.Value.ToString(); + if (BitOrder == BitOrder.MostSignificant) + { + ulong shiftedMask = 0; + for (int i = 0; i < BitCount; i++) + { + int logicalBit = BitOffset + i; + int physicalBit = (logicalBit & ~7) + (7 - (logicalBit & 7)); + shiftedMask |= 1UL << physicalBit; + } + + string fieldMask = FormatMask(shiftedMask, workingWidth); + int shift = backingWidth - BitCount - BitOffset; + string value = $"unchecked(({unsignedType})({valueExpression})) & {valueMaskLiteral}"; + string alignedValue = backingWidth switch + { + 8 => $"({value}) << {shift}", + 16 => $"unchecked((UInt32)BinaryPrimitives.ReverseEndianness(unchecked((UInt16)(({value}) << {shift}))))", + 32 => $"BinaryPrimitives.ReverseEndianness(({value}) << {shift})", + 64 => $"BinaryPrimitives.ReverseEndianness(({value}) << {shift})", + _ => throw new NotSupportedException() + }; + + expression = + $"{{4}} = unchecked(({backingType})((unchecked(({unsignedType}){{4}}) & ~{fieldMask}) | {alignedValue}))"; + return true; + } + + int lsbShift = BitOffset; + ulong lsbShiftedMask = valueMask << lsbShift; + string lsbFieldMask = FormatMask(lsbShiftedMask, workingWidth); + expression = - $"{{4}} = unchecked(({backingType})((unchecked(({unsignedType}){{4}}) & ~{fieldMask}) | " + - $"((unchecked(({unsignedType})({valueExpression})) << {shift}) & {fieldMask})))"; + $"{{4}} = unchecked(({backingType})((unchecked(({unsignedType}){{4}}) & ~{lsbFieldMask}) | " + + $"((unchecked(({unsignedType})({valueExpression})) << {lsbShift}) & {lsbFieldMask})))"; return true; } private bool TryGetDirectIntegralInfo( + out int backingWidth, out int workingWidth, out string unsignedType) { - int backingWidth = FieldType switch + backingWidth = FieldType switch { BitFieldType.SByte or BitFieldType.Byte => 8, BitFieldType.Int16 or BitFieldType.UInt16 => 16, diff --git a/BitsKit.Tests/GeneratorTests.Models.cs b/BitsKit.Tests/GeneratorTests.Models.cs index 6d73f88..22194a2 100644 --- a/BitsKit.Tests/GeneratorTests.Models.cs +++ b/BitsKit.Tests/GeneratorTests.Models.cs @@ -190,10 +190,21 @@ public partial struct OptimizedIntegralAccessorStruct [BitField("ByteValue", 5)] public byte ByteBacking; + [BitField(2)] + [BitField("ReversedByteValue", 5, ReverseBitOrder = true)] + public byte ReversedByteBacking; + + [BitField("ReversedFullByteValue", 8, ReverseBitOrder = true)] + public byte ReversedFullByteBacking; + [BitField(3)] [BitField("ShortValue", 9)] public short ShortBacking; + [BitField(3)] + [BitField("ReversedShortValue", 9, ReverseBitOrder = true)] + public short ReversedShortBacking; + [BitField(5)] [BitField("IntValue", 11)] public int IntBacking; @@ -209,6 +220,9 @@ public partial struct OptimizedIntegralAccessorStruct [BitField("ReversedUInt64Value", 43, ReverseBitOrder = true)] public ulong ReversedUInt64Backing; + [BitField("ReversedFullUInt64Value", 64, ReverseBitOrder = true)] + public ulong ReversedFullUInt64Backing; + [BitField(5)] [BooleanField("Flag")] public uint BooleanBacking; @@ -221,9 +235,17 @@ public partial struct OptimizedIntegralAccessorStruct [BooleanField("ReversedSignedFlag", ReverseBitOrder = true)] public int ReversedSignedBooleanBacking; + [BitField(5)] + [BooleanField("ReversedFlag", ReverseBitOrder = true)] + public uint ReversedBooleanBacking; + [BitField(5)] [EnumField("EnumValue", 2, typeof(TestEnum))] public uint EnumBacking; + + [BitField(5)] + [EnumField("ReversedEnumValue", 2, typeof(TestEnum), ReverseBitOrder = true)] + public uint ReversedEnumBacking; } [BitObject(BitOrder.LeastSignificant)] diff --git a/BitsKit.Tests/GeneratorTests.cs b/BitsKit.Tests/GeneratorTests.cs index 2e235aa..10939ae 100644 --- a/BitsKit.Tests/GeneratorTests.cs +++ b/BitsKit.Tests/GeneratorTests.cs @@ -234,27 +234,39 @@ public void OptimizedIntegralAccessorsMatchBitPrimitives() var actual = new OptimizedIntegralAccessorStruct { ByteBacking = (byte)random.Next(), + ReversedByteBacking = (byte)random.Next(), + ReversedFullByteBacking = (byte)random.Next(), ShortBacking = (short)random.Next(), + ReversedShortBacking = (short)random.Next(), IntBacking = random.Next(), ReversedIntBacking = random.Next(), UInt64Backing = ((ulong)(uint)random.Next() << 32) | (uint)random.Next(), ReversedUInt64Backing = ((ulong)(uint)random.Next() << 32) | (uint)random.Next(), + ReversedFullUInt64Backing = ((ulong)(uint)random.Next() << 32) | (uint)random.Next(), BooleanBacking = (uint)random.Next(), SignedBooleanBacking = random.Next(), ReversedSignedBooleanBacking = random.Next(), - EnumBacking = (uint)random.Next() + ReversedBooleanBacking = (uint)random.Next(), + EnumBacking = (uint)random.Next(), + ReversedEnumBacking = (uint)random.Next() }; Assert.AreEqual(BitPrimitives.ReadUInt8LSB(actual.ByteBacking, 2, 5), actual.ByteValue); + Assert.AreEqual(BitPrimitives.ReadUInt8MSB(actual.ReversedByteBacking, 2, 5), actual.ReversedByteValue); + Assert.AreEqual(BitPrimitives.ReadUInt8MSB(actual.ReversedFullByteBacking, 0, 8), actual.ReversedFullByteValue); Assert.AreEqual(BitPrimitives.ReadInt16LSB(actual.ShortBacking, 3, 9), actual.ShortValue); + Assert.AreEqual(BitPrimitives.ReadInt16MSB(actual.ReversedShortBacking, 3, 9), actual.ReversedShortValue); Assert.AreEqual(BitPrimitives.ReadInt32LSB(actual.IntBacking, 5, 11), actual.IntValue); Assert.AreEqual(BitPrimitives.ReadInt32MSB(actual.ReversedIntBacking, 5, 11), actual.ReversedIntValue); Assert.AreEqual(BitPrimitives.ReadUInt64LSB(actual.UInt64Backing, 0, 64), actual.FullUInt64Value); Assert.AreEqual(BitPrimitives.ReadUInt64MSB(actual.ReversedUInt64Backing, 7, 43), actual.ReversedUInt64Value); + Assert.AreEqual(BitPrimitives.ReadUInt64MSB(actual.ReversedFullUInt64Backing, 0, 64), actual.ReversedFullUInt64Value); 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(BitPrimitives.ReadUInt32MSB(actual.ReversedBooleanBacking, 5, 1) != 0, actual.ReversedFlag); Assert.AreEqual((TestEnum)BitPrimitives.ReadUInt32LSB(actual.EnumBacking, 5, 2), actual.EnumValue); + Assert.AreEqual((TestEnum)BitPrimitives.ReadUInt32MSB(actual.ReversedEnumBacking, 5, 2), actual.ReversedEnumValue); byte byteValue = (byte)random.Next(); byte expectedByte = actual.ByteBacking; @@ -262,12 +274,28 @@ public void OptimizedIntegralAccessorsMatchBitPrimitives() actual.ByteValue = byteValue; Assert.AreEqual(expectedByte, actual.ByteBacking); + byte expectedReversedByte = actual.ReversedByteBacking; + BitPrimitives.WriteUInt8MSB(ref expectedReversedByte, 2, byteValue, 5); + actual.ReversedByteValue = byteValue; + Assert.AreEqual(expectedReversedByte, actual.ReversedByteBacking); + + byte fullByteValue = (byte)random.Next(); + byte expectedReversedFullByte = actual.ReversedFullByteBacking; + BitPrimitives.WriteUInt8MSB(ref expectedReversedFullByte, 0, fullByteValue, 8); + actual.ReversedFullByteValue = fullByteValue; + Assert.AreEqual(expectedReversedFullByte, actual.ReversedFullByteBacking); + short shortValue = (short)random.Next(); short expectedShort = actual.ShortBacking; BitPrimitives.WriteInt16LSB(ref expectedShort, 3, shortValue, 9); actual.ShortValue = shortValue; Assert.AreEqual(expectedShort, actual.ShortBacking); + short expectedReversedShort = actual.ReversedShortBacking; + BitPrimitives.WriteInt16MSB(ref expectedReversedShort, 3, shortValue, 9); + actual.ReversedShortValue = shortValue; + Assert.AreEqual(expectedReversedShort, actual.ReversedShortBacking); + int intValue = random.Next(); int expectedInt = actual.IntBacking; BitPrimitives.WriteInt32LSB(ref expectedInt, 5, intValue, 11); @@ -290,6 +318,12 @@ public void OptimizedIntegralAccessorsMatchBitPrimitives() actual.ReversedUInt64Value = reversedUInt64Value; Assert.AreEqual(expectedReversedUInt64, actual.ReversedUInt64Backing); + ulong reversedFullUInt64Value = ((ulong)(uint)random.Next() << 32) | (uint)random.Next(); + ulong expectedReversedFullUInt64 = actual.ReversedFullUInt64Backing; + BitPrimitives.WriteUInt64MSB(ref expectedReversedFullUInt64, 0, reversedFullUInt64Value, 64); + actual.ReversedFullUInt64Value = reversedFullUInt64Value; + Assert.AreEqual(expectedReversedFullUInt64, actual.ReversedFullUInt64Backing); + bool flag = random.Next(2) != 0; uint expectedBoolean = actual.BooleanBacking; BitPrimitives.WriteUInt32LSB(ref expectedBoolean, 5, flag ? 1u : 0u, 1); @@ -306,11 +340,21 @@ public void OptimizedIntegralAccessorsMatchBitPrimitives() actual.ReversedSignedFlag = flag; Assert.AreEqual(expectedReversedSignedBoolean, actual.ReversedSignedBooleanBacking); + uint expectedReversedBoolean = actual.ReversedBooleanBacking; + BitPrimitives.WriteUInt32MSB(ref expectedReversedBoolean, 5, flag ? 1u : 0u, 1); + actual.ReversedFlag = flag; + Assert.AreEqual(expectedReversedBoolean, actual.ReversedBooleanBacking); + TestEnum enumValue = (TestEnum)random.Next(4); uint expectedEnum = actual.EnumBacking; BitPrimitives.WriteUInt32LSB(ref expectedEnum, 5, (uint)enumValue, 2); actual.EnumValue = enumValue; Assert.AreEqual(expectedEnum, actual.EnumBacking); + + uint expectedReversedEnum = actual.ReversedEnumBacking; + BitPrimitives.WriteUInt32MSB(ref expectedReversedEnum, 5, (uint)enumValue, 2); + actual.ReversedEnumValue = enumValue; + Assert.AreEqual(expectedReversedEnum, actual.ReversedEnumBacking); } } @@ -496,7 +540,7 @@ public Int32 Generated05 public Int32 Generated06 { get => unchecked((Int32)((unchecked((Int32)((BinaryPrimitives.ReverseEndianness(unchecked((UInt32)BackingField00)) << 12) >> 30)) << 30) >> 30)); - set => BitPrimitives.WriteInt32MSB(ref BackingField00, 12, value, 2); + set => BackingField00 = unchecked((Int32)((unchecked((UInt32)BackingField00) & ~0xC00U) | BinaryPrimitives.ReverseEndianness((unchecked((UInt32)(value)) & 0x3U) << 18))); } protected Int32 Generated07 @@ -642,7 +686,7 @@ public Int32 Generated05 public Int32 Generated06 { get => unchecked((Int32)((unchecked((Int32)((BinaryPrimitives.ReverseEndianness(unchecked((UInt32)BackingField00)) << 12) >> 30)) << 30) >> 30)); - set => BitPrimitives.WriteInt32MSB(ref BackingField00, 12, value, 2); + set => BackingField00 = unchecked((Int32)((unchecked((UInt32)BackingField00) & ~0xC00U) | BinaryPrimitives.ReverseEndianness((unchecked((UInt32)(value)) & 0x3U) << 18))); } public required Int32 Generated07 diff --git a/CHANGELOG.md b/CHANGELOG.md index 254e6da..aa93a78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ Notable changes to the community-maintained fork are documented here. This proje ### Changed - 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. +- The source generator emits direct mask-and-shift setters for valid fixed-width LSB and MSB scalar fields, while retaining `BitPrimitives` for memory-backed, native-integer, 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 diff --git a/README.md b/README.md index ce17165..6d42432 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ Enum bit-fields are supported by the `[EnumFieldAttribute]` helper attribute. Th #### Modifiers The `BitFieldModifiers` enum allows alterations to the way that the source generator produces the bit-fields. By default all bit-fields are generated as a *public read/write* or *public readonly* properties relative to their backing field's accessibility. The `Modifiers` field allows control over this and provides the ability to change a bit-field's accessibility and if it is `readonly`, `init only` (.NET 6.0) and/or `required` (.NET 7.0). -For valid fixed-width integral backing fields using least-significant bit order, generated setters use direct masks and shifts. Getters, memory-backed fields, native integers, reversed bit order, and invalid ranges continue through `BitPrimitives` to preserve their established semantics. +For valid fixed-width integral backing fields, generated LSB and MSB getters and setters use direct masks, shifts, and byte-order operations. Memory-backed fields, native integers, and invalid ranges continue through `BitPrimitives` to preserve their established semantics. **Note:** Currently both the getter and setter share the same accessibility therefore you cannot have public bit-fields with private setters. diff --git a/eng/Run-Benchmark-Regression.ps1 b/eng/Run-Benchmark-Regression.ps1 index 09ecb26..aba1838 100644 --- a/eng/Run-Benchmark-Regression.ps1 +++ b/eng/Run-Benchmark-Regression.ps1 @@ -83,14 +83,18 @@ function Get-ComparableBenchmarkMethods { 'GeneratedAccessorGetEnumLSB', 'GeneratedAccessorGetUInt64LSB', 'GeneratedAccessorGetUInt32MSB', + 'GeneratedAccessorSetUInt32MSB', + 'GeneratedAccessorSetUInt64MSB', + 'GeneratedAccessorSetBooleanMSB', + 'GeneratedAccessorSetEnumMSB', 'GeneratedAccessorGetMemoryLSB', 'GeneratedAccessorSetMemoryLSB', 'GeneratedAccessorGetInlineArrayLSB') { $methods.Add($method) } - if ($methods.Count -ne 66) { - throw "Expected 66 comparable benchmark methods, found $($methods.Count)." + if ($methods.Count -ne 70) { + throw "Expected 70 comparable benchmark methods, found $($methods.Count)." } return $methods