Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions BitsKit.Benchmarks/BitsKitBenchmark.GeneratedAccessors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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;
Expand Down
12 changes: 12 additions & 0 deletions BitsKit.Benchmarks/GeneratedAccessorBenchmarkModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
48 changes: 40 additions & 8 deletions BitsKit.Generator/Models/BitFieldModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ protected bool TryGetDirectIntegralReadExpression(out string expression)
expression = string.Empty;

if (!TryGetDirectIntegralInfo(
out _,
out int workingWidth,
out string unsignedType))
{
Expand Down Expand Up @@ -250,6 +251,7 @@ protected bool TryGetDirectIntegralBooleanReadExpression(out string expression)
expression = string.Empty;

if (!TryGetDirectIntegralInfo(
out _,
out int workingWidth,
out string unsignedType))
{
Expand Down Expand Up @@ -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,
Expand Down
22 changes: 22 additions & 0 deletions BitsKit.Tests/GeneratorTests.Models.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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)]
Expand Down
50 changes: 47 additions & 3 deletions BitsKit.Tests/GeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -234,40 +234,68 @@ 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;
BitPrimitives.WriteUInt8LSB(ref expectedByte, 2, byteValue, 5);
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);
Expand All @@ -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);
Expand All @@ -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);
}
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
8 changes: 6 additions & 2 deletions eng/Run-Benchmark-Regression.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading