using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Utilities; using System.Numerics; using System.Runtime.CompilerServices; using System.Text; namespace Misaki.HighPerformance.LowLevel.Collections; public struct UnsafeBitSet : IDisposable { private const int _BIT_SIZE = sizeof(uint) * 8 - 1; // 31 private const int _INDEX_SIZE = 5; // log_2(BitSize + 1) private const int _MASK = (1 << 5) - 1; // 0x1F, the mask to get the bit index inside a uint private static readonly int s_padding = Vector.Count; // The padding used for vectorization, the amount of uints required for being vectorized basically /// /// Determines the required length of an to hold the passed id or bit. /// /// The id or bit. /// A size of required s for the bitset. public static int RequiredLength(int id) { return (id >> 5) + int.Sign(id & _BIT_SIZE); } /// /// Rounds the given length to the next padding size. /// /// The length to round. /// The rounded length. public static int RoundToPadding(int length) { return (length + s_padding - 1) / s_padding * s_padding; } /// /// The bits from the bitset. /// private UnsafeArray _bits; /// /// The highest bit set. /// private int _highestBit; /// /// The maximum -index current in use. /// private int _max; /// /// Initializes a new instance of the class. /// public UnsafeBitSet() { _bits = new UnsafeArray(s_padding, Allocator.Persistent, AllocationOption.Clear); } /// /// Initializes a new instance of the class. /// public UnsafeBitSet(int minimalLength, Allocator allocator, AllocationOption option = AllocationOption.Clear) { var uints = (minimalLength >> _INDEX_SIZE) + int.Sign(minimalLength & _BIT_SIZE); var length = RoundToPadding(uints); _bits = new UnsafeArray(length, allocator, option); } /// /// Initializes a new instance of the class. /// public UnsafeBitSet(Span bits, Allocator allocator, AllocationOption option = AllocationOption.Clear) { _bits = new UnsafeArray(bits.Length, allocator, option); _bits.CopyFrom(bits); _highestBit = 0; _max = _bits.Count * (_BIT_SIZE + 1) - 1; // Calculate the maximum index in use for (var i = 0; i < _bits.Count; i++) { if (_bits[i] != 0) { _highestBit = Math.Max(_highestBit, i * (_BIT_SIZE + 1) + BitOperations.Log2(_bits[i]) + 1); } } } /// /// The highest uint index in use inside the -array. /// public int HighestIndex { get => _max; } /// /// The highest bit set. /// public int HighestBit { get => _highestBit; } /// /// Returns the length of the bitset, how many ints it consists of. /// public int Length { get => _bits.Count; } public bool IsCreated { get => _bits.IsCreated; } /// /// Checks whether a bit is set at the index. /// /// The index. /// True if it is, otherwise false public bool IsSet(int index) { var b = index >> _INDEX_SIZE; if (b >= _bits.Count) { return false; } return (_bits[b] & 1 << (index & _BIT_SIZE)) != 0; } /// /// Sets a bit at the given index. /// Resizes its internal array if necessary. /// /// The index. public void SetBit(int index) { var b = index >> _INDEX_SIZE; if (b >= _bits.Count) { _bits.Resize(RoundToPadding(b)); } // Track highest set bit _highestBit = Math.Max(_highestBit, index); _max = _highestBit / (_BIT_SIZE + 1) + 1; _bits[b] |= 1u << (index & _BIT_SIZE); } /// /// Clears the bit at the given index. /// /// The index. public void ClearBit(int index) { var b = index >> _INDEX_SIZE; if (b >= _bits.Count) { return; } _bits[b] &= ~(1u << (index & _BIT_SIZE)); } /// /// Sets all bits. /// public void SetAll() { var count = _bits.Count; for (var i = 0; i < count; i++) { _bits[i] = 0xffffffff; } _highestBit = _bits.Count * (_BIT_SIZE + 1) - 1; _max = _highestBit / (_BIT_SIZE + 1) + 1; } /// /// Clears all set bits. /// public void ClearAll() { _bits.Clear(); _highestBit = 0; _max = 0; } /// /// Finds the next set bit at or after `startIndex`, or -1 if none. /// public int NextSetBit(int startIndex) { var wordIndex = startIndex >> _BIT_SIZE; if (wordIndex >= _bits.Count) { return -1; } // Mask off bits below startIndex in the first word: var word = _bits[wordIndex] & ~0u << (startIndex & _MASK); while (true) { if (word != 0) { // get the least-significant set bit var bit = BitOperations.TrailingZeroCount(word); return (wordIndex << _BIT_SIZE) + bit; } wordIndex++; if (wordIndex >= _bits.Count) { return -1; } word = _bits[wordIndex]; } } /// /// Checks if all bits from this instance match those of the other instance. /// /// The other . /// True if they match, false if not. [SkipLocalsInit] public bool All(UnsafeBitSet other) { var min = Math.Min(Math.Min(Length, other.Length), _max); if (!Vector.IsHardwareAccelerated || min < s_padding) { var bits = _bits.AsSpan(); var otherBits = other._bits.AsSpan(); // Bitwise and for (var i = 0; i < min; i++) { var bit = bits[i]; if ((bit & otherBits[i]) != bit) { return false; } } // Handle extra bits on our side that might just be all zero. for (var i = min; i < _max; i++) { if (bits[i] != 0) { return false; } } } else { // Vectorized bitwise and for (var i = 0; i < min; i += s_padding) { var vector = new Vector(_bits.AsSpan()[i..]); var otherVector = new Vector(other._bits.AsSpan()[i..]); var resultVector = Vector.BitwiseAnd(vector, otherVector); if (!Vector.EqualsAll(resultVector, vector)) { return false; } } // Handle extra bits on our side that might just be all zero. for (var i = min; i < _max; i += s_padding) { var vector = new Vector(_bits.AsSpan()[i..]); if (!Vector.EqualsAll(vector, Vector.Zero)) // Vectors are not zero bits[0] != 0 basically { return false; } } } return true; } /// /// Checks if any bits from this instance match those of the other instance. /// /// The other . /// True if they match, false if not. public bool Any(UnsafeBitSet other) { var min = Math.Min(Math.Min(Length, other.Length), _max); if (!Vector.IsHardwareAccelerated || min < s_padding) { var bits = _bits.AsSpan(); var otherBits = other._bits.AsSpan(); // Bitwise and, return true since any is met for (var i = 0; i < min; i++) { var bit = bits[i]; if ((bit & otherBits[i]) > 0) { return true; } } // Handle extra bits on our side that might just be all zero. for (var i = min; i < _max; i++) { if (bits[i] > 0) { return false; } } } else { // Vectorized bitwise and, return true since any is met for (var i = 0; i < min; i += s_padding) { var vector = new Vector(_bits.AsSpan()[i..]); var otherVector = new Vector(other._bits.AsSpan()[i..]); var resultVector = Vector.BitwiseAnd(vector, otherVector); if (!Vector.EqualsAll(resultVector, Vector.Zero)) { return true; } } // Handle extra bits on our side that might just be all zero. for (var i = min; i < _max; i += s_padding) { var vector = new Vector(_bits.AsSpan()[i..]); if (!Vector.EqualsAll(vector, Vector.Zero)) // Vectors are not zero bits[0] != 0 basically { return false; } } } return _highestBit <= 0; } /// /// Checks if none bits from this instance match those of the other instance. /// /// The other . /// True if none match, false if not. public bool None(UnsafeBitSet other) { var min = Math.Min(Math.Min(Length, other.Length), _max); if (!Vector.IsHardwareAccelerated || min < s_padding) { var bits = _bits.AsSpan(); var otherBits = other._bits.AsSpan(); // Bitwise and, return true since any is met for (var i = 0; i < min; i++) { var bit = bits[i]; if ((bit & otherBits[i]) != 0) { return false; } } } else { // Vectorized bitwise and, return true since any is met for (var i = 0; i < min; i += s_padding) { var vector = new Vector(_bits.AsSpan()[i..]); var otherVector = new Vector(other._bits.AsSpan()[i..]); var resultVector = Vector.BitwiseAnd(vector, otherVector); if (!Vector.EqualsAll(resultVector, Vector.Zero)) { return false; } } } return true; } /// /// Checks if exactly all bits from this instance match those of the other instance. /// /// The other . /// True if they match, false if not. public bool Exclusive(UnsafeBitSet other) { var min = Math.Min(Math.Min(Length, other.Length), _max); if (!Vector.IsHardwareAccelerated || min < s_padding) { var bits = _bits.AsSpan(); var otherBits = other._bits.AsSpan(); // Bitwise xor, if both are not totally equal, return false for (var i = 0; i < min; i++) { var bit = bits[i]; if ((bit ^ otherBits[i]) != 0) { return false; } } // handle extra bits on our side that might just be all zero for (var i = min; i < _max; i++) { if (bits[i] != 0) { return false; } } } else { // Vectorized bitwise xor, return true since any is met for (var i = 0; i < min; i += s_padding) { var vector = new Vector(_bits.AsSpan()[i..]); var otherVector = new Vector(other._bits.AsSpan()[i..]); var resultVector = Vector.Xor(vector, otherVector); if (!Vector.EqualsAll(resultVector, Vector.Zero)) { return false; } } // Handle extra bits on our side that might just be all zero. for (var i = min; i < _max; i += s_padding) { var vector = new Vector(_bits.AsSpan()[i..]); if (!Vector.EqualsAll(vector, Vector.Zero)) // Vectors are not zero bits[0] != 0 basically { return false; } } } return true; } public unsafe void AndOperation(UnsafeBitSet other) { var min = Math.Min(Length, other.Length); var temp = stackalloc uint[min]; if (!Vector.IsHardwareAccelerated || min < s_padding) { for (var i = 0; i < min; i++) { temp[i] = _bits[i] & other._bits[i]; } } else { for (var i = 0; i < min; i += s_padding) { var vectorLeft = new Vector(_bits.AsSpan()[i..]); var vectorRight = new Vector(other._bits.AsSpan()[i..]); var resultVector = Vector.BitwiseAnd(vectorLeft, vectorRight); resultVector.CopyTo(new Span(temp + i, s_padding)); } } _bits.CopyFrom(new Span(temp, min)); } public unsafe void OrOperation(UnsafeBitSet other) { var min = Math.Min(Length, other.Length); var temp = stackalloc uint[min]; if (!Vector.IsHardwareAccelerated || min < s_padding) { for (var i = 0; i < min; i++) { temp[i] = _bits[i] | other._bits[i]; } } else { for (var i = 0; i < min; i += s_padding) { var vectorLeft = new Vector(_bits.AsSpan()[i..]); var vectorRight = new Vector(other._bits.AsSpan()[i..]); var resultVector = Vector.BitwiseOr(vectorLeft, vectorRight); resultVector.CopyTo(new Span(temp + i, s_padding)); } } _bits.CopyFrom(new Span(temp, min)); } public unsafe void XorOperation(UnsafeBitSet other) { var min = Math.Min(Length, other.Length); var temp = stackalloc uint[min]; if (!Vector.IsHardwareAccelerated || min < s_padding) { for (var i = 0; i < min; i++) { temp[i] = _bits[i] ^ other._bits[i]; } } else { for (var i = 0; i < min; i += s_padding) { var vectorLeft = new Vector(_bits.AsSpan()[i..]); var vectorRight = new Vector(other._bits.AsSpan()[i..]); var resultVector = Vector.Xor(vectorLeft, vectorRight); resultVector.CopyTo(new Span(temp + i, s_padding)); } } _bits.CopyFrom(new Span(temp, min)); } public static UnsafeBitSet operator &(UnsafeBitSet left, UnsafeBitSet right) { var min = Math.Min(left.Length, right.Length); var result = new UnsafeBitSet(min, Allocator.Persistent); if (!Vector.IsHardwareAccelerated || min < s_padding) { for (var i = 0; i < min; i++) { result._bits[i] = left._bits[i] & right._bits[i]; } } else { for (var i = 0; i < min; i += s_padding) { var vectorLeft = new Vector(left._bits.AsSpan()[i..]); var vectorRight = new Vector(right._bits.AsSpan()[i..]); var resultVector = Vector.BitwiseAnd(vectorLeft, vectorRight); resultVector.CopyTo(result._bits.AsSpan(i, s_padding)); } } return result; } public static UnsafeBitSet operator |(UnsafeBitSet left, UnsafeBitSet right) { var min = Math.Min(left.Length, right.Length); var result = new UnsafeBitSet(min, Allocator.Persistent); if (!Vector.IsHardwareAccelerated || min < s_padding) { for (var i = 0; i < min; i++) { result._bits[i] = left._bits[i] | right._bits[i]; } } else { for (var i = 0; i < min; i += s_padding) { var vectorLeft = new Vector(left._bits.AsSpan()[i..]); var vectorRight = new Vector(right._bits.AsSpan()[i..]); var resultVector = Vector.BitwiseOr(vectorLeft, vectorRight); resultVector.CopyTo(result._bits.AsSpan(i, s_padding)); } } return result; } public static UnsafeBitSet operator ~(UnsafeBitSet bitSet) { if (!Vector.IsHardwareAccelerated || bitSet.Length < s_padding) { for (var i = 0; i < bitSet.Length; i++) { bitSet._bits[i] = ~bitSet._bits[i]; } } else { for (var i = 0; i < bitSet.Length; i += s_padding) { var vector = new Vector(bitSet._bits.AsSpan()[i..]); var resultVector = ~vector; resultVector.CopyTo(bitSet._bits.AsSpan(i, s_padding)); } } return bitSet; } /// /// Creates a to access the . /// /// The hash. public readonly Span AsSpan() { var max = _highestBit / (_BIT_SIZE + 1) + 1; return _bits.AsSpan()[..max]; } /// /// Copies the bits into a and returns a slice containing the copied . /// /// The to copy into. /// If true, it will zero the unused space from the . /// The . public Span AsSpan(Span span, bool zero = true) { // Copy everything thats possible from one to another var length = Math.Min(Length, span.Length); for (var index = 0; index < length; index++) { span[index] = _bits[index]; } // Zero the rest space which was not overriden due to the copy. for (var index = length; zero && index < span.Length; index++) { span[index] = 0; } return span[..Length]; } public override string ToString() { // Convert uint to binary form for pretty printing var binaryBuilder = new StringBuilder(); foreach (var bit in _bits) { binaryBuilder.Append(Convert.ToString(bit, 2).PadLeft(32, '0')).Append(','); } binaryBuilder.Length--; return $"{nameof(_bits)}: {binaryBuilder}, {nameof(Length)}: {Length}"; } public void Dispose() { _bits.Dispose(); _highestBit = 0; _max = 0; } } /// /// The struct /// represents a non resizable collection of bits. /// Used to set, check and clear bits on a allocated or on the stack. /// public readonly ref struct SpanBitSet { private const int _BIT_SIZE = sizeof(uint) * 8 - 1; // 31 // NOTE: Is a byte not 8 bits? private const int _BYTE_SIZE = 5; // log_2(BitSize + 1) /// /// The bits from the bitset. /// private readonly Span _bits; /// /// Initializes a new instance of the class. /// public SpanBitSet(Span bits) { _bits = bits; } /// /// Checks whether a bit is set at the index. /// /// The index. /// True if it is, otherwise false public bool IsSet(int index) { var b = index >> _BYTE_SIZE; if (b >= _bits.Length) { return false; } return (_bits[b] & 1 << (index & _BIT_SIZE)) != 0; } /// /// Sets a bit at the given index. /// Resizes its internal array if necessary. /// /// The index. public void SetBit(int index) { var b = index >> _BYTE_SIZE; if (b >= _bits.Length) { return; } _bits[b] |= 1u << (index & _BIT_SIZE); } /// /// Clears the bit at the given index. /// /// The index. public void ClearBit(int index) { var b = index >> _BYTE_SIZE; if (b >= _bits.Length) { return; } _bits[b] &= ~(1u << (index & _BIT_SIZE)); } /// /// Sets all bits. /// public void SetAll() { var count = _bits.Length; for (var i = 0; i < count; i++) { _bits[i] = 0xffffffff; } } /// /// Clears all set bits. /// public void ClearAll() { _bits.Clear(); } /// /// Creates a to access the . /// /// The hash. public Span AsSpan() { return _bits; } /// /// Copies the bits into a and returns a slice containing the copied . /// /// /// The hash. public Span AsSpan(Span span, bool zero = true) { // Prevent exception because target array is to small for copy operation var length = Math.Min(_bits.Length, span.Length); for (var index = 0; index < length; index++) { span[index] = _bits[index]; } // Zero the rest space which was not overriden due to the copy. for (var index = length; zero && index < span.Length; index++) { span[index] = 0; } return span[.._bits.Length]; } public override string ToString() { // Convert uint to binary form for pretty printing var binaryBuilder = new StringBuilder(); foreach (var bit in _bits) { binaryBuilder.Append(Convert.ToString(bit, 2).PadLeft(32, '0')).Append(','); } binaryBuilder.Length--; return $"{nameof(_bits)}: {string.Join(",", binaryBuilder)}"; } }