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
133 changes: 133 additions & 0 deletions BitsKit.Benchmarks/BitsKitBenchmark.BatchPrimitives.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
using BenchmarkDotNet.Attributes;
using BitsKit.Primitives;

namespace BitsKit.Benchmarks;

public partial class BitsKitBenchmark
{
private const int BatchOperations = 4096;
private const int BatchBitOffset = 3;
private const int BatchBitCount = 12;
private const int BatchBitStride = 32;

private readonly byte[] _batchReadBuffer = CreateBatchBuffer();
private readonly byte[] _batchScalarWriteBuffer = CreateBatchBuffer();
private readonly byte[] _batchWriteBuffer = CreateBatchBuffer();
private readonly uint[] _batchScalarResults = new uint[BatchOperations];
private readonly uint[] _batchResults = new uint[BatchOperations];
private readonly uint[] _batchValues = CreateBatchValues();

[Benchmark(OperationsPerInvoke = BatchOperations)]
[BenchmarkCategory("BatchPrimitives", "ScalarLoop", "Read", "Contiguous", "LSB")]
public uint BatchReadUInt32LSBScalarLoop()
{
for (int i = 0; i < _batchScalarResults.Length; i++)
{
_batchScalarResults[i] = BitPrimitives.ReadUInt32LSB(
_batchReadBuffer,
BatchBitOffset + i * BatchBitCount,
BatchBitCount);
}

return _batchScalarResults[BatchOperations - 1];
}

[Benchmark(OperationsPerInvoke = BatchOperations)]
[BenchmarkCategory("BatchPrimitives", "Batch", "Read", "Contiguous", "LSB")]
public uint BatchReadUInt32LSB()
{
BitBatchPrimitives.ReadUInt32LSB(
_batchReadBuffer,
BatchBitOffset,
BatchBitCount,
_batchResults);
return _batchResults[BatchOperations - 1];
}

[Benchmark(OperationsPerInvoke = BatchOperations)]
[BenchmarkCategory("BatchPrimitives", "GeneratedAccessor", "Read", "Contiguous", "LSB")]
public uint BatchGeneratedReadUInt32LSB()
{
GeneratedBatchAccessorModel.ReadValueBatch(_batchReadBuffer, _batchResults);
return _batchResults[BatchOperations - 1];
}

[Benchmark(OperationsPerInvoke = BatchOperations)]
[BenchmarkCategory("BatchPrimitives", "ScalarLoop", "Read", "Strided", "LSB")]
public uint BatchStridedReadUInt32LSBScalarLoop()
{
for (int i = 0; i < _batchScalarResults.Length; i++)
{
_batchScalarResults[i] = BitPrimitives.ReadUInt32LSB(
_batchReadBuffer,
BatchBitOffset + i * BatchBitStride,
BatchBitCount);
}

return _batchScalarResults[BatchOperations - 1];
}

[Benchmark(OperationsPerInvoke = BatchOperations)]
[BenchmarkCategory("BatchPrimitives", "Batch", "Read", "Strided", "LSB")]
public uint BatchStridedReadUInt32LSB()
{
BitBatchPrimitives.ReadUInt32LSB(
_batchReadBuffer,
BatchBitOffset,
BatchBitCount,
BatchBitStride,
_batchResults);
return _batchResults[BatchOperations - 1];
}

[Benchmark(OperationsPerInvoke = BatchOperations)]
[BenchmarkCategory("BatchPrimitives", "ScalarLoop", "Write", "Contiguous", "LSB")]
public byte BatchWriteUInt32LSBScalarLoop()
{
for (int i = 0; i < _batchValues.Length; i++)
{
BitPrimitives.WriteUInt32LSB(
_batchScalarWriteBuffer,
BatchBitOffset + i * BatchBitCount,
_batchValues[i],
BatchBitCount);
}

return _batchScalarWriteBuffer[0];
}

[Benchmark(OperationsPerInvoke = BatchOperations)]
[BenchmarkCategory("BatchPrimitives", "Batch", "Write", "Contiguous", "LSB")]
public byte BatchWriteUInt32LSB()
{
BitBatchPrimitives.WriteUInt32LSB(
_batchWriteBuffer,
BatchBitOffset,
BatchBitCount,
_batchValues);
return _batchWriteBuffer[0];
}

[Benchmark(OperationsPerInvoke = BatchOperations)]
[BenchmarkCategory("BatchPrimitives", "GeneratedAccessor", "Write", "Contiguous", "LSB")]
public byte BatchGeneratedWriteUInt32LSB()
{
GeneratedBatchAccessorModel.WriteValueBatch(_batchWriteBuffer, _batchValues);
return _batchWriteBuffer[0];
}

private static byte[] CreateBatchBuffer()
{
var buffer = new byte[((BatchOperations - 1) * BatchBitStride + BatchBitOffset + BatchBitCount + 7) / 8];
FillBuffer(buffer);
return buffer;
}

private static uint[] CreateBatchValues()
{
var values = new uint[BatchOperations];
for (int i = 0; i < values.Length; i++)
values[i] = unchecked((uint)i * 0x9E3779B9u);
return values;
}
}
8 changes: 8 additions & 0 deletions BitsKit.Benchmarks/GeneratedAccessorBenchmarkModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ public enum GeneratedAccessorKind : uint
Seven
}

[BitObject(BitOrder.LeastSignificant, GenerateBatchAccessors = true)]
public partial struct GeneratedBatchAccessorModel
{
[BitField(3)]
[BitField("Value", 12, BitFieldType.UInt32)]
public Memory<byte> BackingField;
}

[BitObject(BitOrder.LeastSignificant)]
public partial struct GeneratedAccessorMemoryModel
{
Expand Down
61 changes: 51 additions & 10 deletions BitsKit.Generator/Models/BitFieldModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,7 @@ public BitFieldModel(AttributeData attributeData, TypeSymbolProcessor? typeSymbo

public void GenerateCSharpSource(StringBuilder sb)
{
string accessor = (Modifiers & BitFieldModifiers.AccessorMask) switch
{
BitFieldModifiers.Public => "public",
BitFieldModifiers.Private => "private",
BitFieldModifiers.Protected => "protected",
BitFieldModifiers.Internal => "internal",
BitFieldModifiers.ProtectedInternal => "protected internal",
BitFieldModifiers.PrivateProtected => "private protected",
_ => "public",
};
string accessor = GetAccessor();

// property
sb.AppendIndentedLine(2,
Expand Down Expand Up @@ -103,6 +94,56 @@ public void GenerateCSharpSource(StringBuilder sb)
.AppendLine();
}

public void GenerateBatchAccessors(StringBuilder sb)
{
string accessor = GetAccessor();
string valueType = ReturnType ?? FieldType!.Value.ToString();
string primitiveName = this is BooleanFieldModel ? "Bit" : FieldType!.Value.ToIntegralName();
string bitCountArgument = this is BooleanFieldModel ? string.Empty : $", {BitCount}";
string readDestination = this is EnumFieldModel
? $"MemoryMarshal.Cast<{valueType}, {FieldType!.Value}>(destination)"
: "destination";
string writeValues = this is EnumFieldModel
? $"MemoryMarshal.Cast<{valueType}, {FieldType!.Value}>(values)"
: "values";

sb.AppendIndentedLine(2,
$"{accessor} static void Read{Name}Batch(ReadOnlySpan<Byte> source, Span<{valueType}> destination) =>")
.AppendIndentedLine(3,
$"BitBatchPrimitives.Read{primitiveName}{BitOrder.ToShortName()}(source, {BitOffset}{bitCountArgument}, {readDestination});")
.AppendLine()
.AppendIndentedLine(2,
$"{accessor} static void Read{Name}Batch(ReadOnlySpan<Byte> source, Int32 bitStride, Span<{valueType}> destination) =>")
.AppendIndentedLine(3,
$"BitBatchPrimitives.Read{primitiveName}{BitOrder.ToShortName()}(source, {BitOffset}{bitCountArgument}, bitStride, {readDestination});")
.AppendLine();

if (IsReadOnly())
return;

sb.AppendIndentedLine(2,
$"{accessor} static void Write{Name}Batch(Span<Byte> destination, ReadOnlySpan<{valueType}> values) =>")
.AppendIndentedLine(3,
$"BitBatchPrimitives.Write{primitiveName}{BitOrder.ToShortName()}(destination, {BitOffset}{bitCountArgument}, {writeValues});")
.AppendLine()
.AppendIndentedLine(2,
$"{accessor} static void Write{Name}Batch(Span<Byte> destination, Int32 bitStride, ReadOnlySpan<{valueType}> values) =>")
.AppendIndentedLine(3,
$"BitBatchPrimitives.Write{primitiveName}{BitOrder.ToShortName()}(destination, {BitOffset}{bitCountArgument}, bitStride, {writeValues});")
.AppendLine();
}

private string GetAccessor() => (Modifiers & BitFieldModifiers.AccessorMask) switch
{
BitFieldModifiers.Public => "public",
BitFieldModifiers.Private => "private",
BitFieldModifiers.Protected => "protected",
BitFieldModifiers.Internal => "internal",
BitFieldModifiers.ProtectedInternal => "protected internal",
BitFieldModifiers.PrivateProtected => "private protected",
_ => "public",
};

/// <summary>
/// Generates a template for the property accessors, type and name
/// <para>
Expand Down
23 changes: 21 additions & 2 deletions BitsKit.Generator/TypeSymbolProcessor.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using System.Collections.Generic;
using Microsoft.CodeAnalysis;
using System.Linq;
using System.Text;
using BitsKit.Generator.Models;
using System.Linq;
using Microsoft.CodeAnalysis;

namespace BitsKit.Generator;

Expand All @@ -13,6 +13,7 @@ internal sealed record TypeSymbolProcessor

public BitOrder DefaultBitOrder { get; }
public BitObjectAccessMode AccessMode { get; }
public bool GenerateBatchAccessors { get; }
public bool IsStruct { get; }
public bool IsInlineArray { get; }

Expand All @@ -36,6 +37,7 @@ public TypeSymbolProcessor(INamedTypeSymbol typeSymbol, AttributeData attribute)

DefaultBitOrder = (BitOrder)attribute.ConstructorArguments[0].Value!;
AccessMode = GetAccessMode(attribute);
GenerateBatchAccessors = GetGenerateBatchAccessors(attribute);
IsStruct = typeSymbol.TypeKind == TypeKind.Struct;
IsInlineArray = HasInlineArrayAttribute(typeSymbol);

Expand All @@ -53,6 +55,17 @@ private static BitObjectAccessMode GetAccessMode(AttributeData attribute)
return BitObjectAccessMode.Checked;
}

private static bool GetGenerateBatchAccessors(AttributeData attribute)
{
foreach (KeyValuePair<string, TypedConstant> argument in attribute.NamedArguments)
{
if (argument.Key == "GenerateBatchAccessors" && argument.Value.Value is bool value)
return value;
}

return false;
}

public void GenerateCSharpSource(StringBuilder sb)
{
sb.AppendIndentedLine(1,
Expand All @@ -64,6 +77,12 @@ public void GenerateCSharpSource(StringBuilder sb)
foreach (BitFieldModel field in Fields)
field.GenerateCSharpSource(sb);

if (GenerateBatchAccessors)
{
foreach (BitFieldModel field in Fields)
field.GenerateBatchAccessors(sb);
}

sb.RemoveLastLine()
.AppendIndentedLine(1, "}")
.AppendLine();
Expand Down
92 changes: 92 additions & 0 deletions BitsKit.Tests/BatchGeneratedAccessorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace BitsKit.Tests;

[TestClass]
public class BatchGeneratedAccessorTests
{
[TestMethod]
public void GeneratedStridedReadersUseDeclaredFieldLayouts()
{
const int RecordStride = 24;
const int RecordCount = 32;
var random = new Random(0x6E4A7);
var source = new byte[RecordCount * 3];
random.NextBytes(source);

var values = new ushort[RecordCount];
var flags = new bool[RecordCount];
var kinds = new TestEnum[RecordCount];
var bigEndianValues = new uint[RecordCount];

BatchAccessorStruct.ReadValueBatch(source, RecordStride, values);
BatchAccessorStruct.ReadFlagBatch(source, RecordStride, flags);
BatchAccessorStruct.ReadKindBatch(source, RecordStride, kinds);
BatchAccessorStruct.ReadBigEndianValueBatch(source, RecordStride, bigEndianValues);

for (int i = 0; i < RecordCount; i++)
{
int recordOffset = i * RecordStride;
Assert.AreEqual((ushort)Helpers.ReadBitsLSB(source, recordOffset + 3, 12), values[i]);
Assert.AreEqual(Helpers.ReadBitsLSB(source, recordOffset + 15, 1) != 0, flags[i]);
int rawKind = (int)Helpers.ReadBitsLSB(source, recordOffset + 16, 2);
Assert.AreEqual((TestEnum)((rawKind << 30) >> 30), kinds[i]);
Assert.AreEqual((uint)Helpers.ReadBitsMSB(source, recordOffset + 5, 11), bigEndianValues[i]);
}
}

[TestMethod]
public void GeneratedStridedWritersUseDeclaredFieldLayouts()
{
const int RecordStride = 24;
const int RecordCount = 32;
var random = new Random(0xBA7C8);
var original = new byte[RecordCount * 3];
random.NextBytes(original);

var values = new ushort[RecordCount];
var flags = new bool[RecordCount];
var kinds = new TestEnum[RecordCount];
for (int i = 0; i < RecordCount; i++)
{
values[i] = unchecked((ushort)random.Next());
flags[i] = (i & 1) != 0;
kinds[i] = (TestEnum)(i & 3);
}

var expected = (byte[])original.Clone();
for (int i = 0; i < RecordCount; i++)
{
int recordOffset = i * RecordStride;
Helpers.WriteBitsLSB(expected, recordOffset + 3, values[i], 12);
Helpers.WriteBitsLSB(expected, recordOffset + 15, flags[i] ? 1UL : 0UL, 1);
Helpers.WriteBitsLSB(expected, recordOffset + 16, (ulong)kinds[i], 2);
}

var actual = (byte[])original.Clone();
BatchAccessorStruct.WriteValueBatch(actual, RecordStride, values);
BatchAccessorStruct.WriteFlagBatch(actual, RecordStride, flags);
BatchAccessorStruct.WriteKindBatch(actual, RecordStride, kinds);

CollectionAssert.AreEqual(expected, actual);
}

[TestMethod]
public void GeneratedPackedOverloadsAdvanceByFieldWidth()
{
const int ValueCount = 16;
var values = new ushort[ValueCount];
for (int i = 0; i < values.Length; i++)
values[i] = (ushort)(i * 71);

int bitLength = 3 + values.Length * 12;
var storage = new byte[(bitLength + 7) / 8];

BatchAccessorStruct.WriteValueBatch(storage, values);

var actual = new ushort[ValueCount];
BatchAccessorStruct.ReadValueBatch(storage, actual);
CollectionAssert.AreEqual(values, actual);
}
}
Loading
Loading