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)")