diff --git a/BitsKit.Benchmarks/BitsKitBenchmark.GeneratedAccessors.cs b/BitsKit.Benchmarks/BitsKitBenchmark.GeneratedAccessors.cs index fa4846c..50a6759 100644 --- a/BitsKit.Benchmarks/BitsKitBenchmark.GeneratedAccessors.cs +++ b/BitsKit.Benchmarks/BitsKitBenchmark.GeneratedAccessors.cs @@ -12,7 +12,9 @@ public partial class BitsKitBenchmark private readonly GeneratedAccessorLsbModel[] _generatedAccessorSetModels = CreateGeneratedAccessorModels(); private readonly GeneratedAccessorMemoryModel[] _generatedAccessorMemoryModels = CreateGeneratedAccessorMemoryModels(); private readonly GeneratedAccessorAlignedMemoryModel[] _generatedAccessorAlignedMemoryModels = CreateGeneratedAccessorAlignedMemoryModels(); + private readonly byte[][] _generatedAccessorAlignedSpanBuffers = CreateGeneratedAccessorAlignedSpanBuffers(); private readonly GeneratedAccessorInlineArrayModel[] _generatedAccessorInlineArrayModels = CreateGeneratedAccessorInlineArrayModels(); + private readonly GeneratedAccessorAlignedInlineArrayModel[] _generatedAccessorAlignedInlineArrayModels = CreateGeneratedAccessorAlignedInlineArrayModels(); [Benchmark(OperationsPerInvoke = AccessorOperations)] [BenchmarkCategory("GeneratedAccessor", "Integral", "Get", "LSB")] @@ -158,6 +160,94 @@ public uint GeneratedAccessorSetMemoryLSB() return BitConverter.ToUInt32(_generatedAccessorMemoryModels[0].BackingField.Span); } + [Benchmark(OperationsPerInvoke = AccessorOperations)] + [BenchmarkCategory("GeneratedAccessor", "Memory", "Boolean", "Get", "LSB")] + public int GeneratedAccessorGetMemoryBooleanLSB() + { + int count = 0; + + for (int i = 0; i < AccessorOperations; i++) + count += _generatedAccessorMemoryModels[i & AccessorModelMask].Flag ? 1 : 0; + + return count; + } + + [Benchmark(OperationsPerInvoke = AccessorOperations)] + [BenchmarkCategory("GeneratedAccessor", "Memory", "Boolean", "Set", "LSB")] + public byte GeneratedAccessorSetMemoryBooleanLSB() + { + for (int i = 0; i < AccessorOperations; i++) + _generatedAccessorMemoryModels[i & AccessorModelMask].Flag = (i & 1) != 0; + + return _generatedAccessorMemoryModels[0].BooleanBackingField.Span[0]; + } + + [Benchmark(OperationsPerInvoke = AccessorOperations)] + [BenchmarkCategory("GeneratedAccessor", "Memory", "ConstantWidth", "Get", "12", "LSB")] + public uint GeneratedAccessorGetMemory12LSB() + { + uint sum = 0; + + for (int i = 0; i < AccessorOperations; i++) + sum += _generatedAccessorMemoryModels[i & AccessorModelMask].Value12; + + return sum; + } + + [Benchmark(OperationsPerInvoke = AccessorOperations)] + [BenchmarkCategory("GeneratedAccessor", "Memory", "ConstantWidth", "Set", "12", "LSB")] + public uint GeneratedAccessorSetMemory12LSB() + { + for (int i = 0; i < AccessorOperations; i++) + _generatedAccessorMemoryModels[i & AccessorModelMask].Value12 = (uint)i; + + return BitConverter.ToUInt32(_generatedAccessorMemoryModels[0].BackingField12.Span); + } + + [Benchmark(OperationsPerInvoke = AccessorOperations)] + [BenchmarkCategory("GeneratedAccessor", "Memory", "ConstantWidth", "Get", "24", "LSB")] + public uint GeneratedAccessorGetMemory24LSB() + { + uint sum = 0; + + for (int i = 0; i < AccessorOperations; i++) + sum += _generatedAccessorMemoryModels[i & AccessorModelMask].Value24; + + return sum; + } + + [Benchmark(OperationsPerInvoke = AccessorOperations)] + [BenchmarkCategory("GeneratedAccessor", "Memory", "ConstantWidth", "Set", "24", "LSB")] + public uint GeneratedAccessorSetMemory24LSB() + { + for (int i = 0; i < AccessorOperations; i++) + _generatedAccessorMemoryModels[i & AccessorModelMask].Value24 = (uint)i; + + return BitConverter.ToUInt32(_generatedAccessorMemoryModels[0].BackingField24.Span); + } + + [Benchmark(OperationsPerInvoke = AccessorOperations)] + [BenchmarkCategory("GeneratedAccessor", "Memory", "ConstantWidth", "Get", "48", "LSB")] + public ulong GeneratedAccessorGetMemory48LSB() + { + ulong sum = 0; + + for (int i = 0; i < AccessorOperations; i++) + sum += _generatedAccessorMemoryModels[i & AccessorModelMask].Value48; + + return sum; + } + + [Benchmark(OperationsPerInvoke = AccessorOperations)] + [BenchmarkCategory("GeneratedAccessor", "Memory", "ConstantWidth", "Set", "48", "LSB")] + public ulong GeneratedAccessorSetMemory48LSB() + { + for (int i = 0; i < AccessorOperations; i++) + _generatedAccessorMemoryModels[i & AccessorModelMask].Value48 = (ulong)i; + + return BitConverter.ToUInt64(_generatedAccessorMemoryModels[0].BackingField48.Span); + } + [Benchmark(OperationsPerInvoke = AccessorOperations)] [BenchmarkCategory("GeneratedAccessor", "Memory", "Aligned", "Get", "UInt32", "LSB")] public uint GeneratedAccessorGetAlignedMemoryUInt32LSB() @@ -202,6 +292,74 @@ public ulong GeneratedAccessorSetAlignedMemoryUInt64LSB() return BitConverter.ToUInt64(_generatedAccessorAlignedMemoryModels[0].UInt64BackingField.Span); } + [Benchmark(OperationsPerInvoke = AccessorOperations)] + [BenchmarkCategory("GeneratedAccessor", "Span", "Aligned", "Get", "UInt32", "LSB")] + public uint GeneratedAccessorGetAlignedSpanUInt32LSB() + { + uint sum = 0; + + for (int i = 0; i < AccessorOperations; i++) + { + var model = new GeneratedAccessorAlignedSpanModel + { + UInt32BackingField = _generatedAccessorAlignedSpanBuffers[i & AccessorModelMask] + }; + sum += model.UInt32Value; + } + + return sum; + } + + [Benchmark(OperationsPerInvoke = AccessorOperations)] + [BenchmarkCategory("GeneratedAccessor", "Span", "Aligned", "Set", "UInt32", "LSB")] + public uint GeneratedAccessorSetAlignedSpanUInt32LSB() + { + for (int i = 0; i < AccessorOperations; i++) + { + var model = new GeneratedAccessorAlignedSpanModel + { + UInt32BackingField = _generatedAccessorAlignedSpanBuffers[i & AccessorModelMask] + }; + model.UInt32Value = (uint)i; + } + + return BitConverter.ToUInt32(_generatedAccessorAlignedSpanBuffers[0]); + } + + [Benchmark(OperationsPerInvoke = AccessorOperations)] + [BenchmarkCategory("GeneratedAccessor", "Span", "Boolean", "Get", "LSB")] + public int GeneratedAccessorGetSpanBooleanLSB() + { + int count = 0; + + for (int i = 0; i < AccessorOperations; i++) + { + var model = new GeneratedAccessorAlignedSpanModel + { + BooleanBackingField = _generatedAccessorAlignedSpanBuffers[i & AccessorModelMask] + }; + count += model.Flag ? 1 : 0; + } + + return count; + } + + [Benchmark(OperationsPerInvoke = AccessorOperations)] + [BenchmarkCategory("GeneratedAccessor", "Span", "Boolean", "Set", "LSB")] + public byte GeneratedAccessorSetSpanBooleanLSB() + { + for (int i = 0; i < AccessorOperations; i++) + { + var model = new GeneratedAccessorAlignedSpanModel + { + BooleanBackingField = _generatedAccessorAlignedSpanBuffers[i & AccessorModelMask] + }; + model.Flag = (i & 1) != 0; + } + + return _generatedAccessorAlignedSpanBuffers[0][0]; + } + [Benchmark(OperationsPerInvoke = AccessorOperations)] [BenchmarkCategory("GeneratedAccessor", "InlineArray", "Get", "LSB")] public uint GeneratedAccessorGetInlineArrayLSB() @@ -214,6 +372,28 @@ public uint GeneratedAccessorGetInlineArrayLSB() return sum; } + [Benchmark(OperationsPerInvoke = AccessorOperations)] + [BenchmarkCategory("GeneratedAccessor", "InlineArray", "Aligned", "Get", "UInt32", "LSB")] + public uint GeneratedAccessorGetAlignedInlineArrayUInt32LSB() + { + uint sum = 0; + + for (int i = 0; i < AccessorOperations; i++) + sum += _generatedAccessorAlignedInlineArrayModels[i & AccessorModelMask].UInt32Value; + + return sum; + } + + [Benchmark(OperationsPerInvoke = AccessorOperations)] + [BenchmarkCategory("GeneratedAccessor", "InlineArray", "Aligned", "Set", "UInt32", "LSB")] + public uint GeneratedAccessorSetAlignedInlineArrayUInt32LSB() + { + for (int i = 0; i < AccessorOperations; i++) + _generatedAccessorAlignedInlineArrayModels[i & AccessorModelMask].UInt32Value = (uint)i; + + return _generatedAccessorAlignedInlineArrayModels[0].UInt32Value; + } + private static GeneratedAccessorLsbModel[] CreateGeneratedAccessorModels() { var models = new GeneratedAccessorLsbModel[AccessorModelCount]; @@ -243,6 +423,10 @@ private static GeneratedAccessorMemoryModel[] CreateGeneratedAccessorMemoryModel { uint value = unchecked((uint)i * 0x9E3779B9u); models[i].BackingField = BitConverter.GetBytes(value); + models[i].BooleanBackingField = new[] { (byte)value }; + models[i].BackingField12 = BitConverter.GetBytes(value); + models[i].BackingField24 = BitConverter.GetBytes(value); + models[i].BackingField48 = BitConverter.GetBytes(((ulong)value << 32) | ~value); } return models; @@ -277,4 +461,30 @@ private static GeneratedAccessorAlignedMemoryModel[] CreateGeneratedAccessorAlig return models; } + + private static byte[][] CreateGeneratedAccessorAlignedSpanBuffers() + { + var buffers = new byte[AccessorModelCount][]; + + for (int i = 0; i < buffers.Length; i++) + buffers[i] = BitConverter.GetBytes(unchecked((uint)i * 0x9E3779B9u)); + + return buffers; + } + + private static GeneratedAccessorAlignedInlineArrayModel[] CreateGeneratedAccessorAlignedInlineArrayModels() + { + var models = new GeneratedAccessorAlignedInlineArrayModel[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 39520bf..ce6edad 100644 --- a/BitsKit.Benchmarks/GeneratedAccessorBenchmarkModels.cs +++ b/BitsKit.Benchmarks/GeneratedAccessorBenchmarkModels.cs @@ -60,6 +60,22 @@ public partial struct GeneratedAccessorMemoryModel [BitField(5)] [BitField("Value", 11, BitFieldType.UInt32)] public Memory BackingField; + + [BitField(5)] + [BooleanField("Flag")] + public Memory BooleanBackingField; + + [BitField(3)] + [BitField("Value12", 12, BitFieldType.UInt32)] + public Memory BackingField12; + + [BitField(5)] + [BitField("Value24", 24, BitFieldType.UInt32)] + public Memory BackingField24; + + [BitField(7)] + [BitField("Value48", 48, BitFieldType.UInt64)] + public Memory BackingField48; } [BitObject(BitOrder.LeastSignificant)] @@ -72,6 +88,17 @@ public partial struct GeneratedAccessorAlignedMemoryModel public Memory UInt64BackingField; } +[BitObject(BitOrder.LeastSignificant)] +public ref partial struct GeneratedAccessorAlignedSpanModel +{ + [BitField("UInt32Value", 32, BitFieldType.UInt32)] + public Span UInt32BackingField; + + [BitField(5)] + [BooleanField("Flag")] + public Span BooleanBackingField; +} + #if NET8_0_OR_GREATER [BitObject(BitOrder.LeastSignificant)] @@ -83,4 +110,12 @@ public partial struct GeneratedAccessorInlineArrayModel private byte _element; } +[BitObject(BitOrder.LeastSignificant)] +[System.Runtime.CompilerServices.InlineArray(4)] +public partial struct GeneratedAccessorAlignedInlineArrayModel +{ + [BitField("UInt32Value", 32, BitFieldType.UInt32)] + private byte _element; +} + #endif diff --git a/BitsKit.Generator/Models/BitFieldModel.cs b/BitsKit.Generator/Models/BitFieldModel.cs index 6601830..26adf9a 100644 --- a/BitsKit.Generator/Models/BitFieldModel.cs +++ b/BitsKit.Generator/Models/BitFieldModel.cs @@ -182,13 +182,13 @@ BackingFieldType.Span or }; /// - /// Creates a direct endian-aware read for byte-aligned, full-width memory fields. + /// Creates a direct endian-aware read for byte-aligned, full-width byte storage. /// - protected bool TryGetDirectMemoryReadExpression(out string expression) + protected bool TryGetDirectStorageReadExpression(out string expression) { expression = string.Empty; - if (!TryGetDirectMemoryInfo(out int width, out string typeName, out string source)) + if (!TryGetDirectStorageInfo(writable: false, out int width, out string typeName, out string source)) return false; if (width == 8) @@ -203,13 +203,13 @@ protected bool TryGetDirectMemoryReadExpression(out string expression) } /// - /// Creates a direct endian-aware write for byte-aligned, full-width memory fields. + /// Creates a direct endian-aware write for byte-aligned, full-width byte storage. /// - protected bool TryGetDirectMemoryWriteExpression(string valueExpression, out string expression) + protected bool TryGetDirectStorageWriteExpression(string valueExpression, out string expression) { expression = string.Empty; - if (!TryGetDirectMemoryInfo(out int width, out string typeName, out string source)) + if (!TryGetDirectStorageInfo(writable: true, out int width, out string typeName, out string source)) return false; string value = $"unchecked(({typeName})({valueExpression}))"; @@ -224,7 +224,7 @@ protected bool TryGetDirectMemoryWriteExpression(string valueExpression, out str return true; } - private bool TryGetDirectMemoryInfo(out int width, out string typeName, out string source) + private bool TryGetDirectStorageInfo(bool writable, out int width, out string typeName, out string source) { width = FieldType switch { @@ -236,15 +236,315 @@ private bool TryGetDirectMemoryInfo(out int width, out string typeName, out stri }; typeName = FieldType?.ToString() ?? string.Empty; + if (!TryGetByteStorageSource(writable, out source)) + return false; + int byteOffset = BitOffset >> 3; - source = byteOffset == 0 ? "{4}.Span" : $"{{4}}.Span.Slice({byteOffset})"; + if (byteOffset != 0) + source += $".Slice({byteOffset})"; - return BackingFieldType == BackingFieldType.Memory && - width != 0 && + return width != 0 && BitCount == width && (BitOffset & 7) == 0; } + private bool TryGetByteStorageSource(bool writable, out string source) + { + source = BackingFieldType switch + { + BackingFieldType.Memory => "{4}.Span", + BackingFieldType.Span => "{4}", + BackingFieldType.InlineArray when BackingField.TypeString == "byte" => + writable ? "((Span)this)" : "((ReadOnlySpan)this)", + _ => string.Empty + }; + + return source.Length != 0; + } + + /// + /// Creates a direct byte test for boolean fields in byte-addressable storage. + /// + protected bool TryGetDirectStorageBooleanReadExpression(out string expression) + { + expression = string.Empty; + if (BackingFieldType == BackingFieldType.Memory) + return false; + + if (!TryGetByteStorageSource(writable: false, out string source)) + return false; + + int byteOffset = BitOffset >> 3; + int bitInByte = BitOffset & 7; + int mask = 1 << (BitOrder == BitOrder.MostSignificant ? 7 - bitInByte : bitInByte); + expression = $"({source}[{byteOffset}] & 0x{mask:X2}) != 0"; + return true; + } + + /// + /// Creates a direct read-modify-write for boolean fields in byte-addressable storage. + /// + protected bool TryGetDirectStorageBooleanWriteTemplate(out string template) + { + template = string.Empty; + if (BackingFieldType == BackingFieldType.Memory) + return false; + + if (!TryGetByteStorageSource(writable: true, out string source)) + return false; + + int byteOffset = BitOffset >> 3; + int bitInByte = BitOffset & 7; + int mask = 1 << (BitOrder == BitOrder.MostSignificant ? 7 - bitInByte : bitInByte); + if (BackingFieldType == BackingFieldType.Span) + { + template = + "{0} {1}\n" + + "{{\n" + + " if (value)\n" + + $" {source}[{byteOffset}] |= 0x{mask:X2};\n" + + " else\n" + + $" {source}[{byteOffset}] &= 0x{255 ^ mask:X2};\n" + + "}}"; + return true; + } + + template = + "{0} {1}\n" + + "{{\n" + + $" Span source = {source};\n" + + $" source[{byteOffset}] = unchecked((Byte)((source[{byteOffset}] & 0x{255 ^ mask:X2}) | " + + $"(value ? 0x{mask:X2} : 0)));\n" + + "}}"; + return true; + } + + /// + /// Creates a fixed-window read for frequently used multi-byte field widths. + /// + protected bool TryGetDirectFixedWidthReadTemplate(out string template) + { + template = string.Empty; + if (!TryGetFixedWidthStorageInfo( + writable: false, + out string source, + out int byteCount, + out int shift, + out string typeName)) + { + return false; + } + + int loadWidth = byteCount <= 4 ? 4 : 8; + string fastWindow = + $"unchecked((UInt64)System.Runtime.CompilerServices.Unsafe.ReadUnaligned(" + + "ref MemoryMarshal.GetReference(source)))"; + if (BitOrder == BitOrder.MostSignificant) + fastWindow = $"BinaryPrimitives.ReverseEndianness({fastWindow}) >> {64 - loadWidth * 8}"; + + string returnType = ReturnType ?? typeName; + int fastShift = BitOrder == BitOrder.MostSignificant ? + loadWidth * 8 - (BitOffset & 7) - BitCount : + shift; + string fastResult = GetFixedWidthResultExpression(returnType, "current", fastShift); + string exactResult = GetFixedWidthResultExpression(returnType, "current", shift); + string exactWindow = GetWindowReadExpression("source", byteCount, BitOrder); + + template = + "{0} {1}\n" + + "{{\n" + + $" ReadOnlySpan source = {source};\n" + + $" if (source.Length < {loadWidth})\n" + + " return ReadExact(source);\n" + + $" UInt64 current = {fastWindow};\n" + + $" return {fastResult};\n" + + "\n" + + " [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]\n" + + $" static {returnType} ReadExact(ReadOnlySpan source)\n" + + " {{\n" + + $" source = source.Slice(0, {byteCount});\n" + + $" UInt64 current = {exactWindow};\n" + + $" return {exactResult};\n" + + " }}\n" + + "}}"; + return true; + } + + /// + /// Creates a fixed-window read-modify-write for frequently used multi-byte field widths. + /// + protected bool TryGetDirectFixedWidthWriteTemplate(string valueExpression, out string template) + { + template = string.Empty; + if (!TryGetFixedWidthStorageInfo( + writable: true, + out string source, + out int byteCount, + out int shift, + out _)) + { + return false; + } + + ulong valueMask = (1UL << BitCount) - 1; + int loadWidth = byteCount <= 4 ? 4 : 8; + int fastShift = BitOrder == BitOrder.MostSignificant ? + loadWidth * 8 - (BitOffset & 7) - BitCount : + shift; + ulong fastFieldMask = valueMask << fastShift; + ulong exactFieldMask = valueMask << shift; + string fastRead = + $"unchecked((UInt64)System.Runtime.CompilerServices.Unsafe.ReadUnaligned(" + + "ref MemoryMarshal.GetReference(source)))"; + string fastWriteValue = "current"; + if (BitOrder == BitOrder.MostSignificant) + { + fastRead = $"BinaryPrimitives.ReverseEndianness({fastRead}) >> {64 - loadWidth * 8}"; + fastWriteValue = + $"BinaryPrimitives.ReverseEndianness(current << {64 - loadWidth * 8})"; + } + + string typeName = FieldType!.Value.ToString(); + string exactRead = GetWindowReadExpression("source", byteCount, BitOrder); + string exactWrites = GetWindowWriteStatements("source", "current", byteCount, BitOrder); + + template = + "{0} {1}\n" + + "{{\n" + + $" Span source = {source};\n" + + $" if (source.Length < {loadWidth})\n" + + " {{\n" + + $" WriteExact(source, unchecked(({typeName})({valueExpression})));\n" + + " return;\n" + + " }}\n" + + $" UInt64 current = {fastRead};\n" + + $" current = (current & ~0x{fastFieldMask:X}UL) | " + + $"((unchecked((UInt64)({valueExpression})) << {fastShift}) & 0x{fastFieldMask:X}UL);\n" + + $" System.Runtime.CompilerServices.Unsafe.WriteUnaligned(ref MemoryMarshal.GetReference(source), unchecked((UInt{loadWidth * 8})({fastWriteValue})));\n" + + "\n" + + " [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]\n" + + $" static void WriteExact(Span source, {typeName} value)\n" + + " {{\n" + + $" source = source.Slice(0, {byteCount});\n" + + $" UInt64 current = {exactRead};\n" + + $" current = (current & ~0x{exactFieldMask:X}UL) | " + + $"((unchecked((UInt64)(value)) << {shift}) & 0x{exactFieldMask:X}UL);\n" + + $" {exactWrites}\n" + + " }}\n" + + "}}"; + return true; + } + + private string GetFixedWidthResultExpression(string returnType, string value, int shift) + { + ulong valueMask = (1UL << BitCount) - 1; + string extracted = $"(({value} >> {shift}) & 0x{valueMask:X}UL)"; + + if (FieldType is BitFieldType.SByte or + BitFieldType.Int16 or + BitFieldType.Int32 or + BitFieldType.Int64) + { + int signShift = 64 - BitCount; + return $"unchecked(({returnType})(unchecked((Int64)({extracted} << {signShift})) >> {signShift}))"; + } + + return $"unchecked(({returnType}){extracted})"; + } + + private bool TryGetFixedWidthStorageInfo( + bool writable, + out string source, + out int byteCount, + out int shift, + out string typeName) + { + source = string.Empty; + byteCount = 0; + shift = 0; + typeName = FieldType?.ToString() ?? string.Empty; + + if (BitCount is not (11 or 12 or 24 or 48) || + FieldType is not (BitFieldType.SByte or + BitFieldType.Byte or + BitFieldType.Int16 or + BitFieldType.UInt16 or + BitFieldType.Int32 or + BitFieldType.UInt32 or + BitFieldType.Int64 or + BitFieldType.UInt64) || + !TryGetByteStorageSource(writable, out source)) + { + return false; + } + + int bitInByte = BitOffset & 7; + byteCount = (bitInByte + BitCount + 7) >> 3; + int byteOffset = BitOffset >> 3; + if (byteOffset != 0) + source += $".Slice({byteOffset})"; + shift = BitOrder == BitOrder.MostSignificant ? + byteCount * 8 - bitInByte - BitCount : + bitInByte; + return true; + } + + private static string GetWindowReadExpression(string source, int byteCount, BitOrder bitOrder) + { + if (bitOrder == BitOrder.LeastSignificant) + { + return byteCount switch + { + 2 => $"unchecked((UInt64)BinaryPrimitives.ReadUInt16LittleEndian({source}))", + 3 => $"unchecked((UInt64)BinaryPrimitives.ReadUInt16LittleEndian({source}) | ((UInt64){source}[2] << 16))", + 4 => $"unchecked((UInt64)BinaryPrimitives.ReadUInt32LittleEndian({source}))", + 6 => $"unchecked((UInt64)BinaryPrimitives.ReadUInt32LittleEndian({source}) | ((UInt64)BinaryPrimitives.ReadUInt16LittleEndian({source}.Slice(4)) << 32))", + 7 => $"unchecked((UInt64)BinaryPrimitives.ReadUInt32LittleEndian({source}) | ((UInt64)BinaryPrimitives.ReadUInt16LittleEndian({source}.Slice(4)) << 32) | ((UInt64){source}[6] << 48))", + _ => throw new NotSupportedException() + }; + } + + return byteCount switch + { + 2 => $"unchecked((UInt64)BinaryPrimitives.ReadUInt16BigEndian({source}))", + 3 => $"unchecked(((UInt64)BinaryPrimitives.ReadUInt16BigEndian({source}) << 8) | {source}[2])", + 4 => $"unchecked((UInt64)BinaryPrimitives.ReadUInt32BigEndian({source}))", + 6 => $"unchecked(((UInt64)BinaryPrimitives.ReadUInt32BigEndian({source}) << 16) | BinaryPrimitives.ReadUInt16BigEndian({source}.Slice(4)))", + 7 => $"unchecked(((UInt64)BinaryPrimitives.ReadUInt32BigEndian({source}) << 24) | ((UInt64)BinaryPrimitives.ReadUInt16BigEndian({source}.Slice(4)) << 8) | {source}[6])", + _ => throw new NotSupportedException() + }; + } + + private static string GetWindowWriteStatements( + string source, + string value, + int byteCount, + BitOrder bitOrder) + { + if (bitOrder == BitOrder.LeastSignificant) + { + return byteCount switch + { + 2 => $"BinaryPrimitives.WriteUInt16LittleEndian({source}, unchecked((UInt16){value}));", + 3 => $"BinaryPrimitives.WriteUInt16LittleEndian({source}, unchecked((UInt16){value})); {source}[2] = unchecked((Byte)({value} >> 16));", + 4 => $"BinaryPrimitives.WriteUInt32LittleEndian({source}, unchecked((UInt32){value}));", + 6 => $"BinaryPrimitives.WriteUInt32LittleEndian({source}, unchecked((UInt32){value})); BinaryPrimitives.WriteUInt16LittleEndian({source}.Slice(4), unchecked((UInt16)({value} >> 32)));", + 7 => $"BinaryPrimitives.WriteUInt32LittleEndian({source}, unchecked((UInt32){value})); BinaryPrimitives.WriteUInt16LittleEndian({source}.Slice(4), unchecked((UInt16)({value} >> 32))); {source}[6] = unchecked((Byte)({value} >> 48));", + _ => throw new NotSupportedException() + }; + } + + return byteCount switch + { + 2 => $"BinaryPrimitives.WriteUInt16BigEndian({source}, unchecked((UInt16){value}));", + 3 => $"BinaryPrimitives.WriteUInt16BigEndian({source}, unchecked((UInt16)({value} >> 8))); {source}[2] = unchecked((Byte){value});", + 4 => $"BinaryPrimitives.WriteUInt32BigEndian({source}, unchecked((UInt32){value}));", + 6 => $"BinaryPrimitives.WriteUInt32BigEndian({source}, unchecked((UInt32)({value} >> 16))); BinaryPrimitives.WriteUInt16BigEndian({source}.Slice(4), unchecked((UInt16){value}));", + 7 => $"BinaryPrimitives.WriteUInt32BigEndian({source}, unchecked((UInt32)({value} >> 24))); BinaryPrimitives.WriteUInt16BigEndian({source}.Slice(4), unchecked((UInt16)({value} >> 8))); {source}[6] = unchecked((Byte){value});", + _ => throw new NotSupportedException() + }; + } + /// /// Creates a specialized scalar read for fixed-width integral backing fields. /// diff --git a/BitsKit.Generator/Models/BooleanFieldModel.cs b/BitsKit.Generator/Models/BooleanFieldModel.cs index 8ca232e..3883eef 100644 --- a/BitsKit.Generator/Models/BooleanFieldModel.cs +++ b/BitsKit.Generator/Models/BooleanFieldModel.cs @@ -30,7 +30,8 @@ public BooleanFieldModel(AttributeData attributeData, TypeSymbolProcessor? typeS protected override string GetGetterTemplate() { - if (TryGetDirectIntegralBooleanReadExpression(out string expression)) + if (TryGetDirectStorageBooleanReadExpression(out string expression) || + TryGetDirectIntegralBooleanReadExpression(out expression)) return "{0} {1} => " + expression + ";"; string template = BackingFieldType == BackingFieldType.Integral ? @@ -42,13 +43,16 @@ protected override string GetGetterTemplate() protected override string GetSetterTemplate() { + if (TryGetDirectStorageBooleanWriteTemplate(out string template)) + return template; + if (TryGetDirectIntegralWriteExpression("value ? 1 : 0", out string expression)) return "{0} {1} => " + expression + ";"; - string template = BackingFieldType == BackingFieldType.Integral ? + string fallbackTemplate = BackingFieldType == BackingFieldType.Integral ? StringConstants.BooleanSetterTemplate : StringConstants.BooleanSpanSetterTemplate; - return string.Format(template, SetterSource(), FieldType); + return string.Format(fallbackTemplate, SetterSource(), FieldType); } } diff --git a/BitsKit.Generator/Models/EnumFieldModel.cs b/BitsKit.Generator/Models/EnumFieldModel.cs index 3b53b21..00edd2c 100644 --- a/BitsKit.Generator/Models/EnumFieldModel.cs +++ b/BitsKit.Generator/Models/EnumFieldModel.cs @@ -50,7 +50,10 @@ public EnumFieldModel(AttributeData attributeData, TypeSymbolProcessor? typeSymb protected override string GetGetterTemplate() { - if (TryGetDirectMemoryReadExpression(out string expression) || + if (TryGetDirectFixedWidthReadTemplate(out string template)) + return template; + + if (TryGetDirectStorageReadExpression(out string expression) || TryGetDirectIntegralReadExpression(out expression)) return $"{{0}} {{1}} => ({ReturnType})({expression});"; @@ -59,7 +62,10 @@ protected override string GetGetterTemplate() protected override string GetSetterTemplate() { - if (TryGetDirectMemoryWriteExpression("value", out string expression) || + if (TryGetDirectFixedWidthWriteTemplate("value", out string template)) + return template; + + if (TryGetDirectStorageWriteExpression("value", out string expression) || TryGetDirectIntegralWriteExpression("value", out expression)) return "{0} {1} => " + expression + ";"; diff --git a/BitsKit.Generator/Models/IntegralFieldModel.cs b/BitsKit.Generator/Models/IntegralFieldModel.cs index 26a591f..cd12996 100644 --- a/BitsKit.Generator/Models/IntegralFieldModel.cs +++ b/BitsKit.Generator/Models/IntegralFieldModel.cs @@ -42,7 +42,10 @@ public IntegralFieldModel(AttributeData attributeData, TypeSymbolProcessor? type protected override string GetGetterTemplate() { - if (TryGetDirectMemoryReadExpression(out string expression) || + if (TryGetDirectFixedWidthReadTemplate(out string template)) + return template; + + if (TryGetDirectStorageReadExpression(out string expression) || TryGetDirectIntegralReadExpression(out expression)) { if (IsTypeCast) @@ -59,7 +62,10 @@ protected override string GetGetterTemplate() protected override string GetSetterTemplate() { - if (TryGetDirectMemoryWriteExpression("value", out string expression) || + if (TryGetDirectFixedWidthWriteTemplate("value", out string template)) + return template; + + if (TryGetDirectStorageWriteExpression("value", out string expression) || TryGetDirectIntegralWriteExpression("value", out expression)) return "{0} {1} => " + expression + ";"; diff --git a/BitsKit.Tests/GeneratorTests.Models.cs b/BitsKit.Tests/GeneratorTests.Models.cs index fbcda80..1c48137 100644 --- a/BitsKit.Tests/GeneratorTests.Models.cs +++ b/BitsKit.Tests/GeneratorTests.Models.cs @@ -294,6 +294,41 @@ public partial struct AlignedMemoryAccessorStruct public Memory EnumBacking; } +[BitObject(BitOrder.LeastSignificant)] +public partial struct SpecializedMemoryAccessorStruct +{ + [BitField(5)] + [BitField("Value11", 11, BitFieldType.UInt32)] + public Memory Backing11; + + [BitField(3)] + [BitField("SignedValue12", 12, BitFieldType.Int32)] + public Memory SignedBacking12; + + [BitField(5)] + [BitField("BigEndianValue24", 24, BitFieldType.UInt32, ReverseBitOrder = true)] + public Memory BigEndianBacking24; + + [BitField(7)] + [BitField("BigEndianValue48", 48, BitFieldType.UInt64, ReverseBitOrder = true)] + public Memory BigEndianBacking48; +} + +[BitObject(BitOrder.LeastSignificant)] +public ref partial struct SpecializedSpanAccessorStruct +{ + [BitField("AlignedValue", 32, BitFieldType.UInt32)] + public Span AlignedBacking; + + [BitField(5)] + [BooleanField("Flag")] + [BooleanField("BigEndianFlag", ReverseBitOrder = true)] + public Span BooleanBacking; + + [BitField("ReadOnlyAlignedValue", 32, BitFieldType.UInt32)] + public ReadOnlySpan ReadOnlyAlignedBacking; +} + #if NET8_0_OR_GREATER [BitObject(BitOrder.LeastSignificant)] @@ -305,4 +340,22 @@ public partial struct OptimizedInlineArrayAccessorStruct private byte _element; } +[BitObject(BitOrder.LeastSignificant)] +[InlineArray(4)] +public partial struct SpecializedAlignedInlineArrayAccessorStruct +{ + [BitField("Value", 32, BitFieldType.UInt32)] + private byte _element; +} + +[BitObject(BitOrder.LeastSignificant)] +[InlineArray(1)] +public partial struct SpecializedBooleanInlineArrayAccessorStruct +{ + [BitField(5)] + [BooleanField("Flag")] + [BooleanField("BigEndianFlag", ReverseBitOrder = true)] + private byte _element; +} + #endif diff --git a/BitsKit.Tests/GeneratorTests.cs b/BitsKit.Tests/GeneratorTests.cs index b089804..8b793e2 100644 --- a/BitsKit.Tests/GeneratorTests.cs +++ b/BitsKit.Tests/GeneratorTests.cs @@ -473,6 +473,108 @@ public void AlignedMemoryAccessorsMatchBitPrimitives() } } + [TestMethod] + public void SpecializedStorageAccessorsMatchReferenceBits() + { + Random random = new(0x5EEC1A1); + + for (int i = 0; i < 1000; i++) + { + byte[] bytes11 = new byte[(i & 1) == 0 ? 2 : 4]; + byte[] signedBytes12 = new byte[(i & 1) == 0 ? 2 : 4]; + byte[] bigEndianBytes24 = new byte[4]; + byte[] bigEndianBytes48 = new byte[(i & 1) == 0 ? 7 : 8]; + random.NextBytes(bytes11); + random.NextBytes(signedBytes12); + random.NextBytes(bigEndianBytes24); + random.NextBytes(bigEndianBytes48); + + var memory = new SpecializedMemoryAccessorStruct + { + Backing11 = bytes11, + SignedBacking12 = signedBytes12, + BigEndianBacking24 = bigEndianBytes24, + BigEndianBacking48 = bigEndianBytes48 + }; + + Assert.AreEqual((uint)Helpers.ReadBitsLSB(bytes11, 5, 11), memory.Value11); + int expectedSigned12 = (int)Helpers.ReadBitsLSB(signedBytes12, 3, 12); + expectedSigned12 = (expectedSigned12 << 20) >> 20; + Assert.AreEqual(expectedSigned12, memory.SignedValue12); + Assert.AreEqual((uint)Helpers.ReadBitsMSB(bigEndianBytes24, 5, 24), memory.BigEndianValue24); + Assert.AreEqual(Helpers.ReadBitsMSB(bigEndianBytes48, 7, 48), memory.BigEndianValue48); + + uint next11 = (uint)random.Next(1 << 11); + int nextSigned12 = random.Next(-(1 << 11), 1 << 11); + uint next24 = unchecked((uint)random.Next()) & 0xFFFFFF; + ulong next48 = unchecked((ulong)random.NextInt64()) & 0xFFFFFFFFFFFFUL; + byte[] expected11 = (byte[])bytes11.Clone(); + byte[] expectedSigned12Bytes = (byte[])signedBytes12.Clone(); + byte[] expected24 = (byte[])bigEndianBytes24.Clone(); + byte[] expected48 = (byte[])bigEndianBytes48.Clone(); + Helpers.WriteBitsLSB(expected11, 5, next11, 11); + Helpers.WriteBitsLSB(expectedSigned12Bytes, 3, unchecked((ulong)nextSigned12), 12); + Helpers.WriteBitsMSB(expected24, 5, next24, 24); + Helpers.WriteBitsMSB(expected48, 7, next48, 48); + + memory.Value11 = next11; + memory.SignedValue12 = nextSigned12; + memory.BigEndianValue24 = next24; + memory.BigEndianValue48 = next48; + + CollectionAssert.AreEqual(expected11, bytes11); + CollectionAssert.AreEqual(expectedSigned12Bytes, signedBytes12); + CollectionAssert.AreEqual(expected24, bigEndianBytes24); + CollectionAssert.AreEqual(expected48, bigEndianBytes48); + + byte[] alignedBytes = new byte[4]; + byte[] booleanBytes = new byte[1]; + random.NextBytes(alignedBytes); + random.NextBytes(booleanBytes); + var span = new SpecializedSpanAccessorStruct + { + AlignedBacking = alignedBytes, + BooleanBacking = booleanBytes, + ReadOnlyAlignedBacking = alignedBytes + }; + + Assert.AreEqual((uint)Helpers.ReadBitsLSB(alignedBytes, 0, 32), span.AlignedValue); + Assert.AreEqual((uint)Helpers.ReadBitsLSB(alignedBytes, 0, 32), span.ReadOnlyAlignedValue); + Assert.AreEqual(Helpers.ReadBitsLSB(booleanBytes, 5, 1) != 0, span.Flag); + Assert.AreEqual(Helpers.ReadBitsMSB(booleanBytes, 6, 1) != 0, span.BigEndianFlag); + + uint nextAligned = unchecked((uint)random.NextInt64()); + bool nextFlag = (i & 1) != 0; + bool nextBigEndianFlag = (i & 2) != 0; + byte[] expectedAligned = (byte[])alignedBytes.Clone(); + byte[] expectedBoolean = (byte[])booleanBytes.Clone(); + Helpers.WriteBitsLSB(expectedAligned, 0, nextAligned, 32); + Helpers.WriteBitsLSB(expectedBoolean, 5, nextFlag ? 1UL : 0UL, 1); + Helpers.WriteBitsMSB(expectedBoolean, 6, nextBigEndianFlag ? 1UL : 0UL, 1); + span.AlignedValue = nextAligned; + span.Flag = nextFlag; + span.BigEndianFlag = nextBigEndianFlag; + CollectionAssert.AreEqual(expectedAligned, alignedBytes); + CollectionAssert.AreEqual(expectedBoolean, booleanBytes); + +#if NET8_0_OR_GREATER + var alignedInline = new SpecializedAlignedInlineArrayAccessorStruct(); + alignedBytes.CopyTo((Span)alignedInline); + Assert.AreEqual((uint)Helpers.ReadBitsLSB(alignedBytes, 0, 32), alignedInline.Value); + alignedInline.Value = nextAligned; + Assert.AreEqual(nextAligned, (uint)Helpers.ReadBitsLSB((ReadOnlySpan)alignedInline, 0, 32)); + + var booleanInline = new SpecializedBooleanInlineArrayAccessorStruct(); + booleanInline[0] = booleanBytes[0]; + Assert.AreEqual(Helpers.ReadBitsLSB(booleanBytes, 5, 1) != 0, booleanInline.Flag); + Assert.AreEqual(Helpers.ReadBitsMSB(booleanBytes, 6, 1) != 0, booleanInline.BigEndianFlag); + booleanInline.Flag = nextFlag; + booleanInline.BigEndianFlag = nextBigEndianFlag; + Assert.AreEqual(expectedBoolean[0], booleanInline[0]); +#endif + } + } + [TestMethod] public void ReadOnlyMemberTest() { @@ -841,13 +943,19 @@ public System.Boolean Generated01 public System.Boolean Generated10 { - readonly get => BitPrimitives.ReadBitLSB(BackingField01, 0); - set => BitPrimitives.WriteBitLSB(BackingField01, 0, value); + readonly get => (BackingField01[0] & 0x01) != 0; + set + { + if (value) + BackingField01[0] |= 0x01; + else + BackingField01[0] &= 0xFE; + } } public System.Boolean Generated20 { - get => BitPrimitives.ReadBitLSB(BackingField02, 0); + get => (BackingField02[0] & 0x01) != 0; } public unsafe System.Boolean Generated30 diff --git a/eng/Run-Benchmark-Regression.ps1 b/eng/Run-Benchmark-Regression.ps1 index 4f9de4e..0c54439 100644 --- a/eng/Run-Benchmark-Regression.ps1 +++ b/eng/Run-Benchmark-Regression.ps1 @@ -89,16 +89,30 @@ function Get-ComparableBenchmarkMethods { 'GeneratedAccessorSetEnumMSB', 'GeneratedAccessorGetMemoryLSB', 'GeneratedAccessorSetMemoryLSB', + 'GeneratedAccessorGetMemoryBooleanLSB', + 'GeneratedAccessorSetMemoryBooleanLSB', + 'GeneratedAccessorGetMemory12LSB', + 'GeneratedAccessorSetMemory12LSB', + 'GeneratedAccessorGetMemory24LSB', + 'GeneratedAccessorSetMemory24LSB', + 'GeneratedAccessorGetMemory48LSB', + 'GeneratedAccessorSetMemory48LSB', 'GeneratedAccessorGetAlignedMemoryUInt32LSB', 'GeneratedAccessorSetAlignedMemoryUInt32LSB', 'GeneratedAccessorGetAlignedMemoryUInt64LSB', 'GeneratedAccessorSetAlignedMemoryUInt64LSB', - 'GeneratedAccessorGetInlineArrayLSB') { + 'GeneratedAccessorGetAlignedSpanUInt32LSB', + 'GeneratedAccessorSetAlignedSpanUInt32LSB', + 'GeneratedAccessorGetSpanBooleanLSB', + 'GeneratedAccessorSetSpanBooleanLSB', + 'GeneratedAccessorGetInlineArrayLSB', + 'GeneratedAccessorGetAlignedInlineArrayUInt32LSB', + 'GeneratedAccessorSetAlignedInlineArrayUInt32LSB') { $methods.Add($method) } - if ($methods.Count -ne 74) { - throw "Expected 74 comparable benchmark methods, found $($methods.Count)." + if ($methods.Count -ne 88) { + throw "Expected 88 comparable benchmark methods, found $($methods.Count)." } return $methods