Skip to content

Instantly share code, notes, and snippets.

@nathan-alden-sr
Last active December 22, 2020 22:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nathan-alden-sr/ca06a749dcfd2f08b124bb7fe35115f2 to your computer and use it in GitHub Desktop.
Save nathan-alden-sr/ca06a749dcfd2f08b124bb7fe35115f2 to your computer and use it in GitHub Desktop.
A struct implementation of a 1024-bit bit array
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.Arm;
using System.Runtime.Intrinsics.X86;
namespace ConsoleApp1
{
/// <summary>A struct-based bit array of exactly 1024 bits.</summary>
/// <remarks>
/// Inspired by
/// <a href="https://docs.microsoft.com/en-us/dotnet/api/system.collections.bitarray?view=net-5.0">System.Collections.BitArray</a>
/// .
/// </remarks>
public unsafe struct BitArray1024
{
private const int SizeInBits = 1024;
private const int SizeInBytes = SizeInBits / 8;
// ReSharper disable once InconsistentNaming
private const int SizeInInt32s = SizeInBytes / BytesPerInt32;
private const int BitsPerInt32 = BytesPerInt32 * 8;
private const int BytesPerInt32 = sizeof(int);
private const uint Vector128ByteCount = 128 / 8;
private const uint Vector128IntCount = Vector128ByteCount / BytesPerInt32;
private const uint Vector256ByteCount = 256 / 8;
private const uint Vector256IntCount = Vector256ByteCount / BytesPerInt32;
private const int BitShiftPerInt32 = 5;
#pragma warning disable 649
private fixed int _data[SizeInInt32s];
#pragma warning restore 649
public BitArray1024(bool defaultValue = false)
{
if (!defaultValue)
{
return;
}
Span<int> span = MemoryMarshal.CreateSpan(ref _data[0], SizeInInt32s);
span.Fill(-1);
// clear high bit values in the last int
_ = Math.DivRem(SizeInBits, 32, out int extraBits);
if (extraBits > 0)
{
span[^1] = (1 << extraBits) - 1;
}
}
public BitArray1024(in BitArray1024 bitArray)
{
this = bitArray;
}
[SuppressMessage("Performance", "CA1822:Mark members as static")]
public readonly int LengthInBits => SizeInBits;
[SuppressMessage("Performance", "CA1822:Mark members as static")]
public readonly int LengthInBytes => SizeInBytes;
public bool this[int index]
{
readonly get => Get(index);
set => Set(index, value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly bool Get(int index)
{
if ((uint)index >= SizeInBits)
{
throw new ArgumentOutOfRangeException(nameof(index), index, null);
}
return (_data[index >> 5] & (1 << index)) != 0;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Set(int index, bool value)
{
if ((uint)index >= SizeInBits)
{
throw new ArgumentOutOfRangeException(nameof(index), index, null);
}
int bitMask = 1 << index;
ref int segment = ref _data[index >> 5];
if (value)
{
segment |= bitMask;
}
else
{
segment &= ~bitMask;
}
}
public void SetAll(bool value) => Unsafe.InitBlock(ref Unsafe.As<int, byte>(ref _data[0]), value ? 0b11111111 : 0, SizeInBytes);
public void And(in BitArray1024 bitArray)
{
// This method uses unsafe code to manipulate data in the BitArrays. To avoid issues with
// buggy code concurrently mutating these instances in a way that could cause memory corruption,
// we snapshot the arrays from both and then operate only on those snapshots, while also validating
// that the count we iterate to is within the bounds of both arrays. We don't care about such code
// corrupting the BitArray data in a way that produces incorrect answers, since BitArray is not meant
// to be thread-safe; we only care about avoiding buffer overruns.
uint i = 0;
if (Avx2.IsSupported)
{
for (; i < SizeInInt32s - (Vector256IntCount - 1u); i += Vector256IntCount)
{
ref int leftData = ref _data[i];
ref int rightData = ref Unsafe.AsRef(in bitArray._data[i]);
Vector256<int> leftVector = Unsafe.As<int, Vector256<int>>(ref leftData);
Vector256<int> rightVector = Unsafe.As<int, Vector256<int>>(ref rightData);
Unsafe.As<int, Vector256<int>>(ref _data[i]) = Avx2.And(leftVector, rightVector);
}
}
else if (Sse2.IsSupported)
{
for (; i < SizeInInt32s - (Vector128IntCount - 1u); i += Vector128IntCount)
{
ref int leftData = ref _data[i];
ref int rightData = ref Unsafe.AsRef(in bitArray._data[i]);
Vector128<int> leftVector = Unsafe.As<int, Vector128<int>>(ref leftData);
Vector128<int> rightVector = Unsafe.As<int, Vector128<int>>(ref rightData);
Unsafe.As<int, Vector128<int>>(ref _data[i]) = Sse2.And(leftVector, rightVector);
}
}
else if (AdvSimd.IsSupported)
{
for (; i < SizeInInt32s - (Vector128IntCount - 1u); i += Vector128IntCount)
{
ref int leftData = ref _data[i];
ref int rightData = ref Unsafe.AsRef(in bitArray._data[i]);
Vector128<int> leftVector = Unsafe.As<int, Vector128<int>>(ref leftData);
Vector128<int> rightVector = Unsafe.As<int, Vector128<int>>(ref rightData);
Unsafe.As<int, Vector128<int>>(ref _data[i]) = AdvSimd.And(leftVector, rightVector);
}
}
for (; i < SizeInInt32s; i++)
{
_data[i] &= bitArray._data[i];
}
}
public void Or(in BitArray1024 bitArray)
{
// This method uses unsafe code to manipulate data in the BitArrays. To avoid issues with
// buggy code concurrently mutating these instances in a way that could cause memory corruption,
// we snapshot the arrays from both and then operate only on those snapshots, while also validating
// that the count we iterate to is within the bounds of both arrays. We don't care about such code
// corrupting the BitArray data in a way that produces incorrect answers, since BitArray is not meant
// to be thread-safe; we only care about avoiding buffer overruns.
uint i = 0;
if (Avx2.IsSupported)
{
for (; i < SizeInInt32s - (Vector256IntCount - 1u); i += Vector256IntCount)
{
ref int leftData = ref _data[i];
ref int rightData = ref Unsafe.AsRef(in bitArray._data[i]);
Vector256<int> leftVector = Unsafe.As<int, Vector256<int>>(ref leftData);
Vector256<int> rightVector = Unsafe.As<int, Vector256<int>>(ref rightData);
Unsafe.As<int, Vector256<int>>(ref _data[i]) = Avx2.Or(leftVector, rightVector);
}
}
else if (Sse2.IsSupported)
{
for (; i < SizeInInt32s - (Vector128IntCount - 1u); i += Vector128IntCount)
{
ref int leftData = ref _data[i];
ref int rightData = ref Unsafe.AsRef(in bitArray._data[i]);
Vector128<int> leftVector = Unsafe.As<int, Vector128<int>>(ref leftData);
Vector128<int> rightVector = Unsafe.As<int, Vector128<int>>(ref rightData);
Unsafe.As<int, Vector128<int>>(ref _data[i]) = Sse2.Or(leftVector, rightVector);
}
}
else if (AdvSimd.IsSupported)
{
for (; i < SizeInInt32s - (Vector128IntCount - 1u); i += Vector128IntCount)
{
ref int leftData = ref _data[i];
ref int rightData = ref Unsafe.AsRef(in bitArray._data[i]);
Vector128<int> leftVector = Unsafe.As<int, Vector128<int>>(ref leftData);
Vector128<int> rightVector = Unsafe.As<int, Vector128<int>>(ref rightData);
Unsafe.As<int, Vector128<int>>(ref _data[i]) = AdvSimd.Or(leftVector, rightVector);
}
}
for (; i < SizeInInt32s; i++)
{
_data[i] |= bitArray._data[i];
}
}
public void Xor(in BitArray1024 bitArray)
{
// This method uses unsafe code to manipulate data in the BitArrays. To avoid issues with
// buggy code concurrently mutating these instances in a way that could cause memory corruption,
// we snapshot the arrays from both and then operate only on those snapshots, while also validating
// that the count we iterate to is within the bounds of both arrays. We don't care about such code
// corrupting the BitArray data in a way that produces incorrect answers, since BitArray is not meant
// to be thread-safe; we only care about avoiding buffer overruns.
uint i = 0;
if (Avx2.IsSupported)
{
for (; i < SizeInInt32s - (Vector256IntCount - 1u); i += Vector256IntCount)
{
ref int leftData = ref _data[i];
ref int rightData = ref Unsafe.AsRef(in bitArray._data[i]);
Vector256<int> leftVector = Unsafe.As<int, Vector256<int>>(ref leftData);
Vector256<int> rightVector = Unsafe.As<int, Vector256<int>>(ref rightData);
Unsafe.As<int, Vector256<int>>(ref _data[i]) = Avx2.Xor(leftVector, rightVector);
}
}
else if (Sse2.IsSupported)
{
for (; i < SizeInInt32s - (Vector128IntCount - 1u); i += Vector128IntCount)
{
ref int leftData = ref _data[i];
ref int rightData = ref Unsafe.AsRef(in bitArray._data[i]);
Vector128<int> leftVector = Unsafe.As<int, Vector128<int>>(ref leftData);
Vector128<int> rightVector = Unsafe.As<int, Vector128<int>>(ref rightData);
Unsafe.As<int, Vector128<int>>(ref _data[i]) = Sse2.Xor(leftVector, rightVector);
}
}
else if (AdvSimd.IsSupported)
{
for (; i < SizeInInt32s - (Vector128IntCount - 1u); i += Vector128IntCount)
{
ref int leftData = ref _data[i];
ref int rightData = ref Unsafe.AsRef(in bitArray._data[i]);
Vector128<int> leftVector = Unsafe.As<int, Vector128<int>>(ref leftData);
Vector128<int> rightVector = Unsafe.As<int, Vector128<int>>(ref rightData);
Unsafe.As<int, Vector128<int>>(ref _data[i]) = AdvSimd.Xor(leftVector, rightVector);
}
}
for (; i < SizeInInt32s; i++)
{
_data[i] ^= bitArray._data[i];
}
}
public void Not()
{
// This method uses unsafe code to manipulate data in the BitArray. To avoid issues with
// buggy code concurrently mutating this instance in a way that could cause memory corruption,
// we snapshot the array then operate only on this snapshot. We don't care about such code
// corrupting the BitArray data in a way that produces incorrect answers, since BitArray is not meant
// to be thread-safe; we only care about avoiding buffer overruns.
uint i = 0;
if (Avx2.IsSupported)
{
Vector256<int> ones = Vector256.Create(-1);
for (; i < SizeInInt32s - (Vector256IntCount - 1u); i += Vector256IntCount)
{
Vector256<int> vector = Unsafe.As<int, Vector256<int>>(ref _data[i]);
Unsafe.As<int, Vector256<int>>(ref _data[i]) = Avx2.Xor(vector, ones);
}
}
else if (Sse2.IsSupported)
{
Vector128<int> ones = Vector128.Create(-1);
for (; i < SizeInInt32s - (Vector128IntCount - 1u); i += Vector128IntCount)
{
Vector128<int> vector = Unsafe.As<int, Vector128<int>>(ref _data[i]);
Unsafe.As<int, Vector128<int>>(ref _data[i]) = Sse2.Xor(vector, ones);
}
}
else if (AdvSimd.IsSupported)
{
for (; i < SizeInInt32s - (Vector128IntCount - 1u); i += Vector128IntCount)
{
Vector128<int> vector = Unsafe.As<int, Vector128<int>>(ref _data[i]);
Unsafe.As<int, Vector128<int>>(ref _data[i]) = AdvSimd.Not(vector);
}
}
for (; i < SizeInInt32s; i++)
{
_data[i] = ~_data[i];
}
}
public void LeftShift(int count)
{
if (count <= 0)
{
if (count < 0)
{
throw new ArgumentOutOfRangeException(nameof(count), count, null);
}
}
int lengthToClear;
if (count < SizeInBits)
{
int lastIndex = (SizeInBits - 1) >> BitShiftPerInt32; // Divide by 32.
lengthToClear = Math.DivRem(count, 32, out int shiftCount);
if (shiftCount == 0)
{
Span<int> sourceSpan = MemoryMarshal.CreateSpan(ref _data[0], SizeInInt32s - lengthToClear);
Span<int> destinationSpan = MemoryMarshal.CreateSpan(ref _data[lengthToClear], lastIndex + 1 - lengthToClear);
sourceSpan.CopyTo(destinationSpan);
}
else
{
int fromindex = lastIndex - lengthToClear;
unchecked
{
while (fromindex > 0)
{
int left = _data[fromindex] << shiftCount;
uint right = (uint)_data[--fromindex] >> (BitsPerInt32 - shiftCount);
_data[lastIndex] = left | (int)right;
lastIndex--;
}
_data[lastIndex] = _data[fromindex] << shiftCount;
}
}
}
else
{
lengthToClear = SizeInInt32s; // Clear all
}
MemoryMarshal.CreateSpan(ref _data[0], lengthToClear).Clear();
}
public void RightShift(int count)
{
if (count <= 0)
{
if (count < 0)
{
throw new ArgumentOutOfRangeException(nameof(count), count, null);
}
}
var toIndex = 0;
if (count < SizeInBits)
{
int fromIndex = Math.DivRem(count, 32, out int shiftCount);
_ = Math.DivRem(SizeInBits, 32, out int extraBits);
if (shiftCount == 0)
{
unchecked
{
// Cannot use `(1u << extraBits) - 1u` as the mask
// because for extraBits == 0, we need the mask to be 111...111, not 0.
// In that case, we are shifting a uint by 32, which could be considered undefined.
// The result of a shift operation is undefined ... if the right operand
// is greater than or equal to the width in bits of the promoted left operand,
// https://docs.microsoft.com/en-us/cpp/c-language/bitwise-shift-operators?view=vs-2017
// However, the compiler protects us from undefined behaviour by constraining the
// right operand to between 0 and width - 1 (inclusive), i.e. right_operand = (right_operand % width).
uint mask = uint.MaxValue >> (BitsPerInt32 - extraBits);
_data[SizeInInt32s - 1] &= (int)mask;
}
Span<int> sourceSpan = MemoryMarshal.CreateSpan(ref _data[fromIndex], SizeInInt32s - fromIndex);
Span<int> destinationSpan = MemoryMarshal.CreateSpan(ref _data[0], SizeInBytes);
sourceSpan.CopyTo(destinationSpan);
toIndex = SizeInInt32s - fromIndex;
}
else
{
const int lastIndex = SizeInInt32s - 1;
unchecked
{
while (fromIndex < lastIndex)
{
uint right = (uint)_data[fromIndex] >> shiftCount;
int left = _data[++fromIndex] << (BitsPerInt32 - shiftCount);
_data[toIndex++] = left | (int)right;
}
uint mask = uint.MaxValue >> (BitsPerInt32 - extraBits);
mask &= (uint)_data[fromIndex];
_data[toIndex++] = (int)(mask >> shiftCount);
}
}
}
MemoryMarshal.CreateSpan(ref _data[toIndex], SizeInInt32s - toIndex).Clear();
}
public override bool Equals(object? obj) => obj is BitArray1024 other && Equals(in other);
public readonly bool Equals(in BitArray1024 bitArray) => this == bitArray;
public override int GetHashCode() => throw new NotSupportedException();
public static bool operator ==(in BitArray1024 left, in BitArray1024 right)
{
Span<int> leftSpan = MemoryMarshal.CreateSpan(ref Unsafe.AsRef(left._data[0]), SizeInInt32s);
Span<int> rightSpan = MemoryMarshal.CreateSpan(ref Unsafe.AsRef(right._data[0]), SizeInInt32s);
return leftSpan.SequenceEqual(rightSpan);
}
public static bool operator !=(in BitArray1024 left, in BitArray1024 right) => !(left == right);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment