diff --git a/BitsKit.Benchmarks/BitsKitBenchmark.GeneratedAccessors.cs b/BitsKit.Benchmarks/BitsKitBenchmark.GeneratedAccessors.cs index b1eb82c..fa4846c 100644 --- a/BitsKit.Benchmarks/BitsKitBenchmark.GeneratedAccessors.cs +++ b/BitsKit.Benchmarks/BitsKitBenchmark.GeneratedAccessors.cs @@ -11,6 +11,7 @@ public partial class BitsKitBenchmark private readonly GeneratedAccessorLsbModel[] _generatedAccessorGetModels = CreateGeneratedAccessorModels(); private readonly GeneratedAccessorLsbModel[] _generatedAccessorSetModels = CreateGeneratedAccessorModels(); private readonly GeneratedAccessorMemoryModel[] _generatedAccessorMemoryModels = CreateGeneratedAccessorMemoryModels(); + private readonly GeneratedAccessorAlignedMemoryModel[] _generatedAccessorAlignedMemoryModels = CreateGeneratedAccessorAlignedMemoryModels(); private readonly GeneratedAccessorInlineArrayModel[] _generatedAccessorInlineArrayModels = CreateGeneratedAccessorInlineArrayModels(); [Benchmark(OperationsPerInvoke = AccessorOperations)] @@ -157,6 +158,50 @@ public uint GeneratedAccessorSetMemoryLSB() return BitConverter.ToUInt32(_generatedAccessorMemoryModels[0].BackingField.Span); } + [Benchmark(OperationsPerInvoke = AccessorOperations)] + [BenchmarkCategory("GeneratedAccessor", "Memory", "Aligned", "Get", "UInt32", "LSB")] + public uint GeneratedAccessorGetAlignedMemoryUInt32LSB() + { + uint sum = 0; + + for (int i = 0; i < AccessorOperations; i++) + sum += _generatedAccessorAlignedMemoryModels[i & AccessorModelMask].UInt32Value; + + return sum; + } + + [Benchmark(OperationsPerInvoke = AccessorOperations)] + [BenchmarkCategory("GeneratedAccessor", "Memory", "Aligned", "Set", "UInt32", "LSB")] + public uint GeneratedAccessorSetAlignedMemoryUInt32LSB() + { + for (int i = 0; i < AccessorOperations; i++) + _generatedAccessorAlignedMemoryModels[i & AccessorModelMask].UInt32Value = (uint)i; + + return BitConverter.ToUInt32(_generatedAccessorAlignedMemoryModels[0].UInt32BackingField.Span); + } + + [Benchmark(OperationsPerInvoke = AccessorOperations)] + [BenchmarkCategory("GeneratedAccessor", "Memory", "Aligned", "Get", "UInt64", "LSB")] + public ulong GeneratedAccessorGetAlignedMemoryUInt64LSB() + { + ulong sum = 0; + + for (int i = 0; i < AccessorOperations; i++) + sum += _generatedAccessorAlignedMemoryModels[i & AccessorModelMask].UInt64Value; + + return sum; + } + + [Benchmark(OperationsPerInvoke = AccessorOperations)] + [BenchmarkCategory("GeneratedAccessor", "Memory", "Aligned", "Set", "UInt64", "LSB")] + public ulong GeneratedAccessorSetAlignedMemoryUInt64LSB() + { + for (int i = 0; i < AccessorOperations; i++) + _generatedAccessorAlignedMemoryModels[i & AccessorModelMask].UInt64Value = (ulong)i; + + return BitConverter.ToUInt64(_generatedAccessorAlignedMemoryModels[0].UInt64BackingField.Span); + } + [Benchmark(OperationsPerInvoke = AccessorOperations)] [BenchmarkCategory("GeneratedAccessor", "InlineArray", "Get", "LSB")] public uint GeneratedAccessorGetInlineArrayLSB() @@ -218,4 +263,18 @@ private static GeneratedAccessorInlineArrayModel[] CreateGeneratedAccessorInline return models; } + + private static GeneratedAccessorAlignedMemoryModel[] CreateGeneratedAccessorAlignedMemoryModels() + { + var models = new GeneratedAccessorAlignedMemoryModel[AccessorModelCount]; + + for (int i = 0; i < models.Length; i++) + { + ulong value = unchecked((ulong)i * 0x9E3779B97F4A7C15UL); + models[i].UInt32BackingField = BitConverter.GetBytes((uint)value); + models[i].UInt64BackingField = BitConverter.GetBytes(value); + } + + return models; + } } diff --git a/BitsKit.Benchmarks/GeneratedAccessorBenchmarkModels.cs b/BitsKit.Benchmarks/GeneratedAccessorBenchmarkModels.cs index 20de6c5..39520bf 100644 --- a/BitsKit.Benchmarks/GeneratedAccessorBenchmarkModels.cs +++ b/BitsKit.Benchmarks/GeneratedAccessorBenchmarkModels.cs @@ -62,6 +62,16 @@ public partial struct GeneratedAccessorMemoryModel public Memory BackingField; } +[BitObject(BitOrder.LeastSignificant)] +public partial struct GeneratedAccessorAlignedMemoryModel +{ + [BitField("UInt32Value", 32, BitFieldType.UInt32)] + public Memory UInt32BackingField; + + [BitField("UInt64Value", 64, BitFieldType.UInt64)] + public Memory UInt64BackingField; +} + #if NET8_0_OR_GREATER [BitObject(BitOrder.LeastSignificant)] diff --git a/BitsKit.Generator/Models/BitFieldModel.cs b/BitsKit.Generator/Models/BitFieldModel.cs index 75e14e9..6601830 100644 --- a/BitsKit.Generator/Models/BitFieldModel.cs +++ b/BitsKit.Generator/Models/BitFieldModel.cs @@ -181,6 +181,70 @@ BackingFieldType.Span or _ => throw new NotSupportedException() }; + /// + /// Creates a direct endian-aware read for byte-aligned, full-width memory fields. + /// + protected bool TryGetDirectMemoryReadExpression(out string expression) + { + expression = string.Empty; + + if (!TryGetDirectMemoryInfo(out int width, out string typeName, out string source)) + return false; + + if (width == 8) + { + expression = $"unchecked(({typeName}){source}[0])"; + return true; + } + + string endianness = BitOrder == BitOrder.MostSignificant ? "BigEndian" : "LittleEndian"; + expression = $"BinaryPrimitives.Read{typeName}{endianness}({source})"; + return true; + } + + /// + /// Creates a direct endian-aware write for byte-aligned, full-width memory fields. + /// + protected bool TryGetDirectMemoryWriteExpression(string valueExpression, out string expression) + { + expression = string.Empty; + + if (!TryGetDirectMemoryInfo(out int width, out string typeName, out string source)) + return false; + + string value = $"unchecked(({typeName})({valueExpression}))"; + if (width == 8) + { + expression = $"{source}[0] = unchecked((Byte){value})"; + return true; + } + + string endianness = BitOrder == BitOrder.MostSignificant ? "BigEndian" : "LittleEndian"; + expression = $"BinaryPrimitives.Write{typeName}{endianness}({source}, {value})"; + return true; + } + + private bool TryGetDirectMemoryInfo(out int width, out string typeName, out string source) + { + width = FieldType switch + { + BitFieldType.SByte or BitFieldType.Byte => 8, + BitFieldType.Int16 or BitFieldType.UInt16 => 16, + BitFieldType.Int32 or BitFieldType.UInt32 => 32, + BitFieldType.Int64 or BitFieldType.UInt64 => 64, + _ => 0 + }; + typeName = FieldType?.ToString() ?? string.Empty; + + int byteOffset = BitOffset >> 3; + source = byteOffset == 0 ? "{4}.Span" : $"{{4}}.Span.Slice({byteOffset})"; + + return BackingFieldType == BackingFieldType.Memory && + width != 0 && + BitCount == width && + (BitOffset & 7) == 0; + } + /// /// Creates a specialized scalar read for fixed-width integral backing fields. /// diff --git a/BitsKit.Generator/Models/EnumFieldModel.cs b/BitsKit.Generator/Models/EnumFieldModel.cs index ea9fbc1..3b53b21 100644 --- a/BitsKit.Generator/Models/EnumFieldModel.cs +++ b/BitsKit.Generator/Models/EnumFieldModel.cs @@ -50,7 +50,8 @@ public EnumFieldModel(AttributeData attributeData, TypeSymbolProcessor? typeSymb protected override string GetGetterTemplate() { - if (TryGetDirectIntegralReadExpression(out string expression)) + if (TryGetDirectMemoryReadExpression(out string expression) || + TryGetDirectIntegralReadExpression(out expression)) return $"{{0}} {{1}} => ({ReturnType})({expression});"; return string.Format(StringConstants.ExplicitGetterTemplate, GetterSource(), ReturnType); @@ -58,7 +59,8 @@ protected override string GetGetterTemplate() protected override string GetSetterTemplate() { - if (TryGetDirectIntegralWriteExpression("value", out string expression)) + if (TryGetDirectMemoryWriteExpression("value", out string expression) || + TryGetDirectIntegralWriteExpression("value", out expression)) return "{0} {1} => " + expression + ";"; return string.Format(StringConstants.ExplicitSetterTemplate, SetterSource(), FieldType); diff --git a/BitsKit.Generator/Models/IntegralFieldModel.cs b/BitsKit.Generator/Models/IntegralFieldModel.cs index 0606e54..26a591f 100644 --- a/BitsKit.Generator/Models/IntegralFieldModel.cs +++ b/BitsKit.Generator/Models/IntegralFieldModel.cs @@ -42,7 +42,8 @@ public IntegralFieldModel(AttributeData attributeData, TypeSymbolProcessor? type protected override string GetGetterTemplate() { - if (TryGetDirectIntegralReadExpression(out string expression)) + if (TryGetDirectMemoryReadExpression(out string expression) || + TryGetDirectIntegralReadExpression(out expression)) { if (IsTypeCast) expression = $"({ReturnType})({expression})"; @@ -58,7 +59,8 @@ protected override string GetGetterTemplate() protected override string GetSetterTemplate() { - if (TryGetDirectIntegralWriteExpression("value", out string expression)) + if (TryGetDirectMemoryWriteExpression("value", out string expression) || + TryGetDirectIntegralWriteExpression("value", out expression)) return "{0} {1} => " + expression + ";"; if (IsTypeCast) diff --git a/BitsKit.Tests/GeneratorTests.Models.cs b/BitsKit.Tests/GeneratorTests.Models.cs index 22194a2..fbcda80 100644 --- a/BitsKit.Tests/GeneratorTests.Models.cs +++ b/BitsKit.Tests/GeneratorTests.Models.cs @@ -256,6 +256,44 @@ public partial struct OptimizedMemoryAccessorStruct public Memory Backing; } +[BitObject(BitOrder.LeastSignificant)] +public partial struct AlignedMemoryAccessorStruct +{ + [BitField("ByteValue", 8, BitFieldType.Byte)] + public Memory ByteBacking; + + [BitField("SignedByteValue", 8, BitFieldType.SByte)] + public Memory SignedByteBacking; + + [BitField("UInt16Value", 16, BitFieldType.UInt16)] + public Memory UInt16Backing; + + [BitField("Int32Value", 32, BitFieldType.Int32)] + public Memory Int32Backing; + + [BitField("UInt64Value", 64, BitFieldType.UInt64)] + public Memory UInt64Backing; + + [BitField("UInt16BigEndianValue", 16, BitFieldType.UInt16, ReverseBitOrder = true)] + public Memory UInt16BigEndianBacking; + + [BitField("Int32BigEndianValue", 32, BitFieldType.Int32, ReverseBitOrder = true)] + public Memory Int32BigEndianBacking; + + [BitField("UInt64BigEndianValue", 64, BitFieldType.UInt64, ReverseBitOrder = true)] + public Memory UInt64BigEndianBacking; + + [BitField(8)] + [BitField("OffsetUInt32Value", 32, BitFieldType.UInt32)] + public Memory OffsetUInt32Backing; + + [BitField("ReadOnlyUInt32Value", 32, BitFieldType.UInt32)] + public ReadOnlyMemory ReadOnlyUInt32Backing; + + [EnumField("EnumValue", 32, typeof(TestEnum))] + public Memory EnumBacking; +} + #if NET8_0_OR_GREATER [BitObject(BitOrder.LeastSignificant)] diff --git a/BitsKit.Tests/GeneratorTests.cs b/BitsKit.Tests/GeneratorTests.cs index 10939ae..b089804 100644 --- a/BitsKit.Tests/GeneratorTests.cs +++ b/BitsKit.Tests/GeneratorTests.cs @@ -385,6 +385,94 @@ public void OptimizedStorageGettersMatchBitPrimitives() } } + [TestMethod] + public void AlignedMemoryAccessorsMatchBitPrimitives() + { + Random random = new(0xA119ED); + + for (int i = 0; i < 1000; i++) + { + byte[] byteBytes = new byte[1]; + byte[] signedByteBytes = new byte[1]; + byte[] uint16Bytes = new byte[2]; + byte[] int32Bytes = new byte[4]; + byte[] uint64Bytes = new byte[8]; + byte[] uint16BigEndianBytes = new byte[2]; + byte[] int32BigEndianBytes = new byte[4]; + byte[] uint64BigEndianBytes = new byte[8]; + byte[] offsetUInt32Bytes = new byte[5]; + byte[] readOnlyUInt32Bytes = new byte[4]; + byte[] enumBytes = new byte[4]; + + foreach (byte[] bytes in new[] + { + byteBytes, signedByteBytes, uint16Bytes, int32Bytes, uint64Bytes, + uint16BigEndianBytes, int32BigEndianBytes, uint64BigEndianBytes, + offsetUInt32Bytes, readOnlyUInt32Bytes, enumBytes + }) + { + random.NextBytes(bytes); + } + + var actual = new AlignedMemoryAccessorStruct + { + ByteBacking = byteBytes, + SignedByteBacking = signedByteBytes, + UInt16Backing = uint16Bytes, + Int32Backing = int32Bytes, + UInt64Backing = uint64Bytes, + UInt16BigEndianBacking = uint16BigEndianBytes, + Int32BigEndianBacking = int32BigEndianBytes, + UInt64BigEndianBacking = uint64BigEndianBytes, + OffsetUInt32Backing = offsetUInt32Bytes, + ReadOnlyUInt32Backing = readOnlyUInt32Bytes, + EnumBacking = enumBytes + }; + + Assert.AreEqual(BitPrimitives.ReadUInt8LSB(byteBytes, 0, 8), actual.ByteValue); + Assert.AreEqual(BitPrimitives.ReadInt8LSB(signedByteBytes, 0, 8), actual.SignedByteValue); + Assert.AreEqual(BitPrimitives.ReadUInt16LSB(uint16Bytes, 0, 16), actual.UInt16Value); + Assert.AreEqual(BitPrimitives.ReadInt32LSB(int32Bytes, 0, 32), actual.Int32Value); + Assert.AreEqual(BitPrimitives.ReadUInt64LSB(uint64Bytes, 0, 64), actual.UInt64Value); + Assert.AreEqual(BitPrimitives.ReadUInt16MSB(uint16BigEndianBytes, 0, 16), actual.UInt16BigEndianValue); + Assert.AreEqual(BitPrimitives.ReadInt32MSB(int32BigEndianBytes, 0, 32), actual.Int32BigEndianValue); + Assert.AreEqual(BitPrimitives.ReadUInt64MSB(uint64BigEndianBytes, 0, 64), actual.UInt64BigEndianValue); + Assert.AreEqual(BitPrimitives.ReadUInt32LSB(offsetUInt32Bytes, 8, 32), actual.OffsetUInt32Value); + Assert.AreEqual(BitPrimitives.ReadUInt32LSB(readOnlyUInt32Bytes, 0, 32), actual.ReadOnlyUInt32Value); + Assert.AreEqual((TestEnum)BitPrimitives.ReadUInt32LSB(enumBytes, 0, 32), actual.EnumValue); + + byte nextByte = (byte)random.Next(); + sbyte nextSignedByte = unchecked((sbyte)random.Next()); + ushort nextUInt16 = (ushort)random.Next(); + int nextInt32 = random.Next(); + ulong nextUInt64 = unchecked((ulong)random.NextInt64()); + uint nextOffsetUInt32 = unchecked((uint)random.NextInt64()); + TestEnum nextEnum = (TestEnum)(uint)random.Next(4); + + actual.ByteValue = nextByte; + actual.SignedByteValue = nextSignedByte; + actual.UInt16Value = nextUInt16; + actual.Int32Value = nextInt32; + actual.UInt64Value = nextUInt64; + actual.UInt16BigEndianValue = nextUInt16; + actual.Int32BigEndianValue = nextInt32; + actual.UInt64BigEndianValue = nextUInt64; + actual.OffsetUInt32Value = nextOffsetUInt32; + actual.EnumValue = nextEnum; + + Assert.AreEqual(nextByte, BitPrimitives.ReadUInt8LSB(byteBytes, 0, 8)); + Assert.AreEqual(nextSignedByte, BitPrimitives.ReadInt8LSB(signedByteBytes, 0, 8)); + Assert.AreEqual(nextUInt16, BitPrimitives.ReadUInt16LSB(uint16Bytes, 0, 16)); + Assert.AreEqual(nextInt32, BitPrimitives.ReadInt32LSB(int32Bytes, 0, 32)); + Assert.AreEqual(nextUInt64, BitPrimitives.ReadUInt64LSB(uint64Bytes, 0, 64)); + Assert.AreEqual(nextUInt16, BitPrimitives.ReadUInt16MSB(uint16BigEndianBytes, 0, 16)); + Assert.AreEqual(nextInt32, BitPrimitives.ReadInt32MSB(int32BigEndianBytes, 0, 32)); + Assert.AreEqual(nextUInt64, BitPrimitives.ReadUInt64MSB(uint64BigEndianBytes, 0, 64)); + Assert.AreEqual(nextOffsetUInt32, BitPrimitives.ReadUInt32LSB(offsetUInt32Bytes, 8, 32)); + Assert.AreEqual(nextEnum, (TestEnum)BitPrimitives.ReadUInt32LSB(enumBytes, 0, 32)); + } + } + [TestMethod] public void ReadOnlyMemberTest() { diff --git a/eng/Run-Benchmark-Regression.ps1 b/eng/Run-Benchmark-Regression.ps1 index aba1838..4f9de4e 100644 --- a/eng/Run-Benchmark-Regression.ps1 +++ b/eng/Run-Benchmark-Regression.ps1 @@ -89,12 +89,16 @@ function Get-ComparableBenchmarkMethods { 'GeneratedAccessorSetEnumMSB', 'GeneratedAccessorGetMemoryLSB', 'GeneratedAccessorSetMemoryLSB', + 'GeneratedAccessorGetAlignedMemoryUInt32LSB', + 'GeneratedAccessorSetAlignedMemoryUInt32LSB', + 'GeneratedAccessorGetAlignedMemoryUInt64LSB', + 'GeneratedAccessorSetAlignedMemoryUInt64LSB', 'GeneratedAccessorGetInlineArrayLSB') { $methods.Add($method) } - if ($methods.Count -ne 70) { - throw "Expected 70 comparable benchmark methods, found $($methods.Count)." + if ($methods.Count -ne 74) { + throw "Expected 74 comparable benchmark methods, found $($methods.Count)." } return $methods