Last active
December 22, 2020 22:30
-
-
Save nathan-alden-sr/ca06a749dcfd2f08b124bb7fe35115f2 to your computer and use it in GitHub Desktop.
A struct implementation of a 1024-bit bit array
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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