From ce3853db5375bdd2314750825b233201b66aefb3 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Tue, 3 Jun 2025 17:40:24 -0400 Subject: [PATCH 1/5] Move BitArray to corelib --- .../GeneratorTests.Helpers.cs | 2 +- .../ref/System.Collections.cs | 61 ++++++++++--------- .../src/System.Collections.csproj | 1 - .../System.Private.CoreLib.Shared.projitems | 1 + .../src/System/Collections/BitArray.cs | 0 5 files changed, 34 insertions(+), 31 deletions(-) rename src/libraries/{System.Collections => System.Private.CoreLib}/src/System/Collections/BitArray.cs (100%) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.Helpers.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.Helpers.cs index 2a79ea181c89e9..1e245d516d2653 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.Helpers.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.Helpers.cs @@ -68,7 +68,6 @@ private static class Diagnostics } private static readonly Assembly[] s_compilationAssemblyRefs = new[] { - typeof(BitArray).Assembly, typeof(ConfigurationBinder).Assembly, typeof(ConfigurationBuilder).Assembly, typeof(CultureInfo).Assembly, @@ -80,6 +79,7 @@ private static class Diagnostics typeof(IDictionary).Assembly, typeof(OptionsBuilder<>).Assembly, typeof(OptionsConfigurationServiceCollectionExtensions).Assembly, + typeof(Stack<>).Assembly, typeof(Uri).Assembly, }; diff --git a/src/libraries/System.Collections/ref/System.Collections.cs b/src/libraries/System.Collections/ref/System.Collections.cs index 186f5d64cad1dd..539f96f5c480c6 100644 --- a/src/libraries/System.Collections/ref/System.Collections.cs +++ b/src/libraries/System.Collections/ref/System.Collections.cs @@ -7,35 +7,6 @@ #if !BUILDING_CORELIB_REFERENCE namespace System.Collections { - public sealed partial class BitArray : System.Collections.ICollection, System.Collections.IEnumerable, System.ICloneable - { - public BitArray(bool[] values) { } - public BitArray(byte[] bytes) { } - public BitArray(System.Collections.BitArray bits) { } - public BitArray(int length) { } - public BitArray(int length, bool defaultValue) { } - public BitArray(int[] values) { } - public int Count { get { throw null; } } - public bool IsReadOnly { get { throw null; } } - public bool IsSynchronized { get { throw null; } } - public bool this[int index] { get { throw null; } set { } } - public int Length { get { throw null; } set { } } - public object SyncRoot { get { throw null; } } - public System.Collections.BitArray And(System.Collections.BitArray value) { throw null; } - public object Clone() { throw null; } - public void CopyTo(System.Array array, int index) { } - public bool Get(int index) { throw null; } - public System.Collections.IEnumerator GetEnumerator() { throw null; } - public bool HasAllSet() { throw null; } - public bool HasAnySet() { throw null; } - public System.Collections.BitArray LeftShift(int count) { throw null; } - public System.Collections.BitArray Not() { throw null; } - public System.Collections.BitArray Or(System.Collections.BitArray value) { throw null; } - public System.Collections.BitArray RightShift(int count) { throw null; } - public void Set(int index, bool value) { } - public void SetAll(bool value) { } - public System.Collections.BitArray Xor(System.Collections.BitArray value) { throw null; } - } public static partial class StructuralComparisons { public static System.Collections.IComparer StructuralComparer { get { throw null; } } @@ -584,6 +555,38 @@ void System.Collections.IEnumerator.Reset() { } } } #endif // !BUILDING_CORELIB_REFERENCE +namespace System.Collections +{ + public sealed partial class BitArray : System.Collections.ICollection, System.Collections.IEnumerable, System.ICloneable + { + public BitArray(bool[] values) { } + public BitArray(byte[] bytes) { } + public BitArray(System.Collections.BitArray bits) { } + public BitArray(int length) { } + public BitArray(int length, bool defaultValue) { } + public BitArray(int[] values) { } + public int Count { get { throw null; } } + public bool IsReadOnly { get { throw null; } } + public bool IsSynchronized { get { throw null; } } + public bool this[int index] { get { throw null; } set { } } + public int Length { get { throw null; } set { } } + public object SyncRoot { get { throw null; } } + public System.Collections.BitArray And(System.Collections.BitArray value) { throw null; } + public object Clone() { throw null; } + public void CopyTo(System.Array array, int index) { } + public bool Get(int index) { throw null; } + public System.Collections.IEnumerator GetEnumerator() { throw null; } + public bool HasAllSet() { throw null; } + public bool HasAnySet() { throw null; } + public System.Collections.BitArray LeftShift(int count) { throw null; } + public System.Collections.BitArray Not() { throw null; } + public System.Collections.BitArray Or(System.Collections.BitArray value) { throw null; } + public System.Collections.BitArray RightShift(int count) { throw null; } + public void Set(int index, bool value) { } + public void SetAll(bool value) { } + public System.Collections.BitArray Xor(System.Collections.BitArray value) { throw null; } + } +} namespace System.Collections.Generic { public static partial class CollectionExtensions diff --git a/src/libraries/System.Collections/src/System.Collections.csproj b/src/libraries/System.Collections/src/System.Collections.csproj index 8e78d63745bf70..bb7cfe2ad83cfb 100644 --- a/src/libraries/System.Collections/src/System.Collections.csproj +++ b/src/libraries/System.Collections/src/System.Collections.csproj @@ -10,7 +10,6 @@ - diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index cea0562929595a..c1293cc2716cf4 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -180,6 +180,7 @@ + diff --git a/src/libraries/System.Collections/src/System/Collections/BitArray.cs b/src/libraries/System.Private.CoreLib/src/System/Collections/BitArray.cs similarity index 100% rename from src/libraries/System.Collections/src/System/Collections/BitArray.cs rename to src/libraries/System.Private.CoreLib/src/System/Collections/BitArray.cs From 18e9de5cb31a1dbeb9b0a1249df2e10fcf1fede8 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Tue, 3 Jun 2025 20:08:31 -0400 Subject: [PATCH 2/5] Add CollectionsMarshal.AsBytes(BitArray) --- .../src/Resources/Strings.resx | 9 - .../tests/BitArray/BitArray_GetSetTests.cs | 55 ++ .../src/Resources/Strings.resx | 8 +- .../src/System/Collections/BitArray.cs | 788 +++++++----------- .../InteropServices/CollectionsMarshal.cs | 25 + .../ref/System.Runtime.InteropServices.cs | 1 + 6 files changed, 390 insertions(+), 496 deletions(-) diff --git a/src/libraries/System.Collections/src/Resources/Strings.resx b/src/libraries/System.Collections/src/Resources/Strings.resx index 1e6e014ed65502..2c580ebd632741 100644 --- a/src/libraries/System.Collections/src/Resources/Strings.resx +++ b/src/libraries/System.Collections/src/Resources/Strings.resx @@ -102,12 +102,6 @@ Mutating a value collection derived from a dictionary is not allowed. - - Array lengths must be the same. - - - Only supported array types for CopyTo on BitArrays are Boolean[], Int32[] and Byte[]. - Hashtable's capacity overflowed and went negative. Check load factor, capacity and the current size of the table. @@ -117,9 +111,6 @@ Only single dimensional arrays are supported for the requested action. - - The input array length must not exceed Int32.MaxValue / {0}. Otherwise BitArray.Length would exceed Int32.MaxValue. - Target array type is not compatible with the type of items in the collection. diff --git a/src/libraries/System.Collections/tests/BitArray/BitArray_GetSetTests.cs b/src/libraries/System.Collections/tests/BitArray/BitArray_GetSetTests.cs index 912164e4efc567..9ea0b3c13cd01c 100644 --- a/src/libraries/System.Collections/tests/BitArray/BitArray_GetSetTests.cs +++ b/src/libraries/System.Collections/tests/BitArray/BitArray_GetSetTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using System.Runtime.InteropServices; using Xunit; namespace System.Collections.Tests @@ -328,6 +329,60 @@ public static void CopyTo(BitArray bitArray, int destinationLength, int start } } + [Fact] + public static void AsBytes_NullInput_ProducesEmptySpan() + { + BitArray? ba = null; + Span bytes = CollectionsMarshal.AsBytes(ba); + Assert.True(bytes.IsEmpty); + } + + [Fact] + public static void AsBytes_RoundtripsCtor() + { + Random r = new(); + + for (int length = 0; length < 128; length++) + { + bool[] bits = new bool[length]; + for (int i = 0; i < bits.Length; i++) + { + bits[i] = r.Next(0, 2) == 0; + } + + BitArray ba = new(bits); + + Span bytes = CollectionsMarshal.AsBytes(ba); + Assert.Equal((length + 7) / 8, bytes.Length); + for (int i = 0; i < length; i++) + { + Assert.Equal(bits[i], (bytes[i / 8] & (1 << (i % 8))) != 0); + } + for (int i = length; i < bytes.Length * 8; i++) + { + Assert.Equal(0, bytes[i / 8] & (1 << (i % 8))); + } + } + } + + [Fact] + public static void AsBytes_ChangesReflectedInBitArray() + { + BitArray ba = new(8); + Span bytes = CollectionsMarshal.AsBytes(ba); + Assert.Equal(0, bytes[0]); + + bytes[0] = 0b11100101; + Assert.True(ba[0]); + Assert.False(ba[1]); + Assert.True(ba[2]); + Assert.False(ba[3]); + Assert.False(ba[4]); + Assert.True(ba[5]); + Assert.True(ba[6]); + Assert.True(ba[7]); + } + [Fact] public static void CopyToByteArray() { diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx index ea16db6230b4f8..c26261f4758f4c 100644 --- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx +++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx @@ -214,6 +214,9 @@ Destination array is not long enough to copy all the items in the collection. Check array index and length. + + Only supported array types for CopyTo on BitArrays are Boolean[], Int32[] and Byte[]. + The array starting from the specified index is not long enough to read a value of the specified type. @@ -879,6 +882,9 @@ Error parsing array spec. + + The input array length must not exceed Int32.MaxValue / {0}. Otherwise BitArray.Length would exceed Int32.MaxValue. + Attribute names must be unique. @@ -4364,4 +4370,4 @@ Dynamic entrypoint allocation is not supported in the current environment. - \ No newline at end of file + diff --git a/src/libraries/System.Private.CoreLib/src/System/Collections/BitArray.cs b/src/libraries/System.Private.CoreLib/src/System/Collections/BitArray.cs index fa515728e085d4..ad6c94c71a0b2b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Collections/BitArray.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Collections/BitArray.cs @@ -11,35 +11,38 @@ namespace System.Collections { - // A vector of bits. Use this to store bits efficiently, without having to do bit - // shifting yourself. + /// + /// Manages a compact array of bit values, which are represented as , where + /// indicates that the bit is on (1) and indicates + /// the bit is off (0). + /// [Serializable] - [System.Runtime.CompilerServices.TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")] + [TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")] public sealed class BitArray : ICollection, ICloneable { - private int[] m_array; // Do not rename (binary serialization) + /// Bit array. Always little endian, even on big endian platforms. + internal int[] m_array; // Do not rename (binary serialization) private int m_length; // Do not rename (binary serialization) private int _version; // Do not rename (binary serialization) - private const int _ShrinkThreshold = 256; - - /*========================================================================= - ** Allocates space to hold length bit values. All of the values in the bit - ** array are set to false. - ** - ** Exceptions: ArgumentException if length < 0. - =========================================================================*/ + /// + /// Initializes a new instance of the class that can hold the specified + /// number of bit values, which are initially set to false. + /// + /// The number of bit values in the new . + /// is less than zero. public BitArray(int length) : this(length, false) { } - /*========================================================================= - ** Allocates space to hold length bit values. All of the values in the bit - ** array are set to defaultValue. - ** - ** Exceptions: ArgumentOutOfRangeException if length < 0. - =========================================================================*/ + /// + /// Initializes a new instance of the class that can hold the specified number of + /// bit values, which are initially set to the specified value. + /// + /// The number of bit values in the new . + /// The Boolean value to assign to each bit. + /// is less than zero. public BitArray(int length, bool defaultValue) { ArgumentOutOfRangeException.ThrowIfNegative(length); @@ -51,32 +54,25 @@ public BitArray(int length, bool defaultValue) { Array.Fill(m_array, -1); - // clear high bit values in the last int - Div32Rem(length, out int extraBits); - if (extraBits > 0) + // Clear high bit values in the last int. + int extraBits = (int)((uint)length % BitsPerInt32); + if (extraBits != 0) { - m_array[^1] = (1 << extraBits) - 1; + m_array[^1] = ReverseIfBE((1 << extraBits) - 1); } } - - _version = 0; } - /*========================================================================= - ** Allocates space to hold the bit values in bytes. bytes[0] represents - ** bits 0 - 7, bytes[1] represents bits 8 - 15, etc. The LSB of each byte - ** represents the lowest index value; bytes[0] & 1 represents bit 0, - ** bytes[0] & 2 represents bit 1, bytes[0] & 4 represents bit 2, etc. - ** - ** Exceptions: ArgumentException if bytes == null. - =========================================================================*/ + /// + /// Initializes a new instance of the class that contains bit values copied + /// from the specified array of bytes. + /// + /// An array of bytes containing the values to copy, where each byte represents eight consecutive bits. + /// is null. + /// The length of in bits is greater than . public BitArray(byte[] bytes) { ArgumentNullException.ThrowIfNull(bytes); - - // this value is chosen to prevent overflow when computing m_length. - // m_length is of type int32 and is exposed as a property, so - // type of m_length can't be changed to accommodate. if (bytes.Length > int.MaxValue / BitsPerByte) { throw new ArgumentException(SR.Format(SR.Argument_ArrayTooLarge, BitsPerByte), nameof(bytes)); @@ -85,36 +81,15 @@ public BitArray(byte[] bytes) m_array = new int[GetInt32ArrayLengthFromByteLength(bytes.Length)]; m_length = bytes.Length * BitsPerByte; - uint totalCount = (uint)bytes.Length / 4; - - ReadOnlySpan byteSpan = bytes; - for (int i = 0; i < totalCount; i++) - { - m_array[i] = BinaryPrimitives.ReadInt32LittleEndian(byteSpan); - byteSpan = byteSpan.Slice(4); - } - - Debug.Assert(byteSpan.Length >= 0 && byteSpan.Length < 4); - - int last = 0; - switch (byteSpan.Length) - { - case 3: - last = byteSpan[2] << 16; - goto case 2; - // fall through - case 2: - last |= byteSpan[1] << 8; - goto case 1; - // fall through - case 1: - m_array[totalCount] = last | byteSpan[0]; - break; - } - - _version = 0; + bytes.AsSpan().CopyTo(MemoryMarshal.AsBytes((Span)m_array)); } + /// + /// Initializes a new instance of the class that contains bit values + /// copied from the specified array of Booleans. + /// + /// An array of Booleans to copy. + /// is null. public BitArray(bool[] values) { ArgumentNullException.ThrowIfNull(values); @@ -124,7 +99,7 @@ public BitArray(bool[] values) uint i = 0; - if (values.Length < Vector256.Count) + if (!BitConverter.IsLittleEndian || values.Length < Vector256.Count) { goto LessThan32; } @@ -178,44 +153,51 @@ public BitArray(bool[] values) { if (values[i]) { - int elementIndex = Div32Rem((int)i, out int extraBits); - m_array[elementIndex] |= 1 << extraBits; + (uint elementIndex, uint extraBits) = Math.DivRem(i, BitsPerInt32); + m_array[elementIndex] |= ReverseIfBE(1 << (int)extraBits); } } - - _version = 0; } - /*========================================================================= - ** Allocates space to hold the bit values in values. values[0] represents - ** bits 0 - 31, values[1] represents bits 32 - 63, etc. The LSB of each - ** integer represents the lowest index value; values[0] & 1 represents bit - ** 0, values[0] & 2 represents bit 1, values[0] & 4 represents bit 2, etc. - ** - ** Exceptions: ArgumentException if values == null. - =========================================================================*/ + /// + /// Initializes a new instance of the class that contains bit values + /// copied from the specified array of 32-bit integers. + /// + /// An array of integers containing the values to copy, where each integer represents 32 consecutive bits. + /// is null. + /// The length of in bits is greater than . + /// + /// The number in the first array element represents bits 0 through 31, the second number in the array represents + /// bits 32 through 63, and so on. The Least Significant Bit of each integer represents the lowest index value: + /// "[0] & 1" represents bit 0, "[0] & 2" represents bit 1, + /// "[0] & 4" represents bit 2, and so on. + /// public BitArray(int[] values) { ArgumentNullException.ThrowIfNull(values); - - // this value is chosen to prevent overflow when computing m_length if (values.Length > int.MaxValue / BitsPerInt32) { throw new ArgumentException(SR.Format(SR.Argument_ArrayTooLarge, BitsPerInt32), nameof(values)); } m_array = new int[values.Length]; - Array.Copy(values, m_array, values.Length); - m_length = values.Length * BitsPerInt32; + if (BitConverter.IsLittleEndian) + { + Array.Copy(values, m_array, values.Length); + } + else + { + BinaryPrimitives.ReverseEndianness(values, m_array); + } - _version = 0; + m_length = values.Length * BitsPerInt32; } - /*========================================================================= - ** Allocates a new BitArray with the same length and bit values as bits. - ** - ** Exceptions: ArgumentException if bits == null. - =========================================================================*/ + /// + /// Initializes a new instance of the class that contains bit values copied from the specified BitArray. + /// + /// The to copy. + /// is null. public BitArray(BitArray bits) { ArgumentNullException.ThrowIfNull(bits); @@ -223,50 +205,62 @@ public BitArray(BitArray bits) int arrayLength = GetInt32ArrayLengthFromBitLength(bits.m_length); m_array = new int[arrayLength]; - Debug.Assert(bits.m_array.Length <= arrayLength); Array.Copy(bits.m_array, m_array, arrayLength); m_length = bits.m_length; - - _version = bits._version; } + /// + /// Gets or sets the value of the bit at a specific position in the . + /// + /// The zero-based index of the value to get or set. + /// The value of the bit at position . + /// The value of the bit at position . + /// is less than zero. + /// is greater than or equal to . public bool this[int index] { get => Get(index); set => Set(index, value); } - /*========================================================================= - ** Returns the bit value at position index. - ** - ** Exceptions: ArgumentOutOfRangeException if index < 0 or - ** index >= GetLength(). - =========================================================================*/ + /// + /// Gets the value of the bit at a specific position in the . + /// + /// The zero-based index of the value to get. + /// The value of the bit at position . + /// is less than zero. + /// is greater than or equal to . [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Get(int index) { if ((uint)index >= (uint)m_length) + { ThrowArgumentOutOfRangeException(index); + } - return (m_array[index >> 5] & (1 << index)) != 0; + return (ReverseIfBE(m_array[index >> BitShiftPerInt32]) & (1 << index)) != 0; } - /*========================================================================= - ** Sets the bit value at position index to value. - ** - ** Exceptions: ArgumentOutOfRangeException if index < 0 or - ** index >= GetLength(). - =========================================================================*/ + /// + /// Sets the value of the bit at a specific position in the . + /// + /// The zero-based index of the value to get. + /// The Boolean value to assign to the bit. + /// is less than zero. + /// is greater than or equal to . [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Set(int index, bool value) { if ((uint)index >= (uint)m_length) + { ThrowArgumentOutOfRangeException(index); + } - int bitMask = 1 << index; - ref int segment = ref m_array[index >> 5]; + int bitMask = ReverseIfBE(1 << index); + + ref int segment = ref m_array[index >> BitShiftPerInt32]; if (value) { @@ -280,9 +274,10 @@ public void Set(int index, bool value) _version++; } - /*========================================================================= - ** Sets all the bit values to value. - =========================================================================*/ + /// + /// Sets all bits in the to the specified value. + /// + /// The Boolean value to assign to all bits. public void SetAll(bool value) { int arrayLength = GetInt32ArrayLengthFromBitLength(Length); @@ -291,11 +286,11 @@ public void SetAll(bool value) { span.Fill(-1); - // clear high bit values in the last int - Div32Rem(m_length, out int extraBits); - if (extraBits > 0) + // Clear high bit values in the last int. + int extraBits = (int)((uint)m_length % BitsPerInt32); + if (extraBits != 0) { - span[^1] &= (1 << extraBits) - 1; + span[^1] &= ReverseIfBE((1 << extraBits) - 1); } } else @@ -306,86 +301,49 @@ public void SetAll(bool value) _version++; } - /*========================================================================= - ** Returns a reference to the current instance ANDed with value. - ** - ** Exceptions: ArgumentException if value == null or - ** value.Length != this.Length. - =========================================================================*/ - public BitArray And(BitArray value) - { - ArgumentNullException.ThrowIfNull(value); - - // 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. - int[] thisArray = m_array; - int[] valueArray = value.m_array; - - int count = GetInt32ArrayLengthFromBitLength(Length); - if (Length != value.Length || (uint)count > (uint)thisArray.Length || (uint)count > (uint)valueArray.Length) - throw new ArgumentException(SR.Arg_ArrayLengthsDiffer); - - // Unroll loop for count less than Vector256 size. - switch (count) - { - case 7: thisArray[6] &= valueArray[6]; goto case 6; - case 6: thisArray[5] &= valueArray[5]; goto case 5; - case 5: thisArray[4] &= valueArray[4]; goto case 4; - case 4: thisArray[3] &= valueArray[3]; goto case 3; - case 3: thisArray[2] &= valueArray[2]; goto case 2; - case 2: thisArray[1] &= valueArray[1]; goto case 1; - case 1: thisArray[0] &= valueArray[0]; goto Done; - case 0: goto Done; - } - uint i = 0; + /// + /// Performs the bitwise AND operation between the elements of the current object and the + /// corresponding elements in the specified array. The current object will be modified to + /// store the result of the bitwise AND operation. + /// + /// The array with which to perform the bitwise AND operation. + /// An array containing the result of the bitwise AND operation, which is a reference to the current object. + /// is null. + /// and the current do not have the same number of elements. + public BitArray And(BitArray value) => Invoke(value); - ref int left = ref MemoryMarshal.GetArrayDataReference(thisArray); - ref int right = ref MemoryMarshal.GetArrayDataReference(valueArray); - if (Vector512.IsHardwareAccelerated && (uint)count >= Vector512.Count) - { - for (; i < (uint)count - (Vector512.Count - 1u); i += (uint)Vector512.Count) - { - Vector512 result = Vector512.LoadUnsafe(ref left, i) & Vector512.LoadUnsafe(ref right, i); - result.StoreUnsafe(ref left, i); - } - } - else if (Vector256.IsHardwareAccelerated && (uint)count >= Vector256.Count) - { - for (; i < (uint)count - (Vector256.Count - 1u); i += (uint)Vector256.Count) - { - Vector256 result = Vector256.LoadUnsafe(ref left, i) & Vector256.LoadUnsafe(ref right, i); - result.StoreUnsafe(ref left, i); - } - } - else if (Vector128.IsHardwareAccelerated && (uint)count >= Vector128.Count) - { - for (; i < (uint)count - (Vector128.Count - 1u); i += (uint)Vector128.Count) - { - Vector128 result = Vector128.LoadUnsafe(ref left, i) & Vector128.LoadUnsafe(ref right, i); - result.StoreUnsafe(ref left, i); - } - } + /// + /// Performs the bitwise OR operation between the elements of the current object and the + /// corresponding elements in the specified array. The current object will be modified to + /// store the result of the bitwise OR operation. + /// + /// The array with which to perform the bitwise OR operation. + /// An array containing the result of the bitwise OR operation, which is a reference to the current object. + /// is null. + /// and the current do not have the same number of elements. + public BitArray Or(BitArray value) => Invoke(value); - for (; i < (uint)count; i++) - thisArray[i] &= valueArray[i]; + /// + /// Performs the bitwise XOR operation between the elements of the current object and the + /// corresponding elements in the specified array. The current object will be modified to + /// store the result of the bitwise XOR operation. + /// + /// The array with which to perform the bitwise XOR operation. + /// An array containing the result of the bitwise XOR operation, which is a reference to the current object. + /// is null. + /// and the current do not have the same number of elements. + public BitArray Xor(BitArray value) => Invoke(value); - Done: - _version++; - return this; - } + /// + /// Inverts all the bit values in the current , so that elements set to true are changed to false, + /// and elements set to false are changed to true. + /// + /// The current instance with inverted bit values. + public BitArray Not() => Invoke(this); // argument is ignored - /*========================================================================= - ** Returns a reference to the current instance ORed with value. - ** - ** Exceptions: ArgumentException if value == null or - ** value.Length != this.Length. - =========================================================================*/ - public BitArray Or(BitArray value) + /// Provides the implementation for , , etc. + private BitArray Invoke(BitArray value) where TBinaryOp : struct, IBinaryOp { ArgumentNullException.ThrowIfNull(value); @@ -397,206 +355,103 @@ public BitArray Or(BitArray value) // to be thread-safe; we only care about avoiding buffer overruns. int[] thisArray = m_array; int[] valueArray = value.m_array; - int count = GetInt32ArrayLengthFromBitLength(Length); - if (Length != value.Length || (uint)count > (uint)thisArray.Length || (uint)count > (uint)valueArray.Length) + + if (Length != value.Length || + (uint)count > (uint)thisArray.Length || + (uint)count > (uint)valueArray.Length) + { throw new ArgumentException(SR.Arg_ArrayLengthsDiffer); + } // Unroll loop for count less than Vector256 size. switch (count) { - case 7: thisArray[6] |= valueArray[6]; goto case 6; - case 6: thisArray[5] |= valueArray[5]; goto case 5; - case 5: thisArray[4] |= valueArray[4]; goto case 4; - case 4: thisArray[3] |= valueArray[3]; goto case 3; - case 3: thisArray[2] |= valueArray[2]; goto case 2; - case 2: thisArray[1] |= valueArray[1]; goto case 1; - case 1: thisArray[0] |= valueArray[0]; goto Done; + case 7: thisArray[6] = TBinaryOp.Invoke(thisArray[6], valueArray[6]); goto case 6; + case 6: thisArray[5] = TBinaryOp.Invoke(thisArray[5], valueArray[5]); goto case 5; + case 5: thisArray[4] = TBinaryOp.Invoke(thisArray[4], valueArray[4]); goto case 4; + case 4: thisArray[3] = TBinaryOp.Invoke(thisArray[3], valueArray[3]); goto case 3; + case 3: thisArray[2] = TBinaryOp.Invoke(thisArray[2], valueArray[2]); goto case 2; + case 2: thisArray[1] = TBinaryOp.Invoke(thisArray[1], valueArray[1]); goto case 1; + case 1: thisArray[0] = TBinaryOp.Invoke(thisArray[0], valueArray[0]); goto Done; case 0: goto Done; } uint i = 0; - ref int left = ref MemoryMarshal.GetArrayDataReference(thisArray); - ref int right = ref MemoryMarshal.GetArrayDataReference(valueArray); - if (Vector512.IsHardwareAccelerated && (uint)count >= Vector512.Count) + if (Vector512.IsHardwareAccelerated) { - for (; i < (uint)count - (Vector512.Count - 1u); i += (uint)Vector512.Count) - { - Vector512 result = Vector512.LoadUnsafe(ref left, i) | Vector512.LoadUnsafe(ref right, i); - result.StoreUnsafe(ref left, i); - } + Apply>(count, ref i, thisArray, valueArray); } - else if (Vector256.IsHardwareAccelerated && (uint)count >= Vector256.Count) + else if (Vector256.IsHardwareAccelerated) { - for (; i < (uint)count - (Vector256.Count - 1u); i += (uint)Vector256.Count) - { - Vector256 result = Vector256.LoadUnsafe(ref left, i) | Vector256.LoadUnsafe(ref right, i); - result.StoreUnsafe(ref left, i); - } + Apply>(count, ref i, thisArray, valueArray); } - else if (Vector128.IsHardwareAccelerated && (uint)count >= Vector128.Count) + else if (Vector128.IsHardwareAccelerated) { - for (; i < (uint)count - (Vector128.Count - 1u); i += (uint)Vector128.Count) - { - Vector128 result = Vector128.LoadUnsafe(ref left, i) | Vector128.LoadUnsafe(ref right, i); - result.StoreUnsafe(ref left, i); - } + Apply>(count, ref i, thisArray, valueArray); } for (; i < (uint)count; i++) - thisArray[i] |= valueArray[i]; + { + thisArray[i] = TBinaryOp.Invoke(thisArray[i], valueArray[i]); + } - Done: + Done: _version++; return this; - } - - /*========================================================================= - ** Returns a reference to the current instance XORed with value. - ** - ** Exceptions: ArgumentException if value == null or - ** value.Length != this.Length. - =========================================================================*/ - public BitArray Xor(BitArray value) - { - ArgumentNullException.ThrowIfNull(value); - - // 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. - int[] thisArray = m_array; - int[] valueArray = value.m_array; - int count = GetInt32ArrayLengthFromBitLength(Length); - if (Length != value.Length || (uint)count > (uint)thisArray.Length || (uint)count > (uint)valueArray.Length) - throw new ArgumentException(SR.Arg_ArrayLengthsDiffer); - - // Unroll loop for count less than Vector256 size. - switch (count) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static void Apply(int count, ref uint i, int[] thisArray, int[] valueArray) + where TVector : ISimdVector { - case 7: thisArray[6] ^= valueArray[6]; goto case 6; - case 6: thisArray[5] ^= valueArray[5]; goto case 5; - case 5: thisArray[4] ^= valueArray[4]; goto case 4; - case 4: thisArray[3] ^= valueArray[3]; goto case 3; - case 3: thisArray[2] ^= valueArray[2]; goto case 2; - case 2: thisArray[1] ^= valueArray[1]; goto case 1; - case 1: thisArray[0] ^= valueArray[0]; goto Done; - case 0: goto Done; - } - - uint i = 0; - - ref int left = ref MemoryMarshal.GetArrayDataReference(thisArray); - ref int right = ref MemoryMarshal.GetArrayDataReference(valueArray); + ref int left = ref MemoryMarshal.GetArrayDataReference(thisArray); + ref int right = ref MemoryMarshal.GetArrayDataReference(valueArray); - if (Vector512.IsHardwareAccelerated && (uint)count >= Vector512.Count) - { - for (; i < (uint)count - (Vector512.Count - 1u); i += (uint)Vector512.Count) + for (; i < (uint)count - (TVector.ElementCount - 1u); i += (uint)TVector.ElementCount) { - Vector512 result = Vector512.LoadUnsafe(ref left, i) ^ Vector512.LoadUnsafe(ref right, i); + TVector result = TBinaryOp.Invoke(TVector.LoadUnsafe(ref left, i), TVector.LoadUnsafe(ref right, i)); result.StoreUnsafe(ref left, i); } } - else if (Vector256.IsHardwareAccelerated && (uint)count >= Vector256.Count) - { - for (; i < (uint)count - (Vector256.Count - 1u); i += (uint)Vector256.Count) - { - Vector256 result = Vector256.LoadUnsafe(ref left, i) ^ Vector256.LoadUnsafe(ref right, i); - result.StoreUnsafe(ref left, i); - } - } - else if (Vector128.IsHardwareAccelerated && (uint)count >= Vector128.Count) - { - for (; i < (uint)count - (Vector128.Count - 1u); i += (uint)Vector128.Count) - { - Vector128 result = Vector128.LoadUnsafe(ref left, i) ^ Vector128.LoadUnsafe(ref right, i); - result.StoreUnsafe(ref left, i); - } - } - - for (; i < (uint)count; i++) - thisArray[i] ^= valueArray[i]; - - Done: - _version++; - return this; } - /*========================================================================= - ** Inverts all the bit values. On/true bit values are converted to - ** off/false. Off/false bit values are turned on/true. The current instance - ** is updated and returned. - =========================================================================*/ - public BitArray Not() + private struct AndBinaryOp : IBinaryOp { - // 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. - int[] thisArray = m_array; - - int count = GetInt32ArrayLengthFromBitLength(Length); - - // Unroll loop for count less than Vector256 size. - switch (count) - { - case 7: thisArray[6] = ~thisArray[6]; goto case 6; - case 6: thisArray[5] = ~thisArray[5]; goto case 5; - case 5: thisArray[4] = ~thisArray[4]; goto case 4; - case 4: thisArray[3] = ~thisArray[3]; goto case 3; - case 3: thisArray[2] = ~thisArray[2]; goto case 2; - case 2: thisArray[1] = ~thisArray[1]; goto case 1; - case 1: thisArray[0] = ~thisArray[0]; goto Done; - case 0: goto Done; - } + public static int Invoke(int value1, int value2) => value1 & value2; + public static TVector Invoke(TVector value1, TVector value2) where TVector : ISimdVector => value1 & value2; + } - uint i = 0; + private struct OrBinaryOp : IBinaryOp + { + public static int Invoke(int value1, int value2) => value1 | value2; + public static TVector Invoke(TVector value1, TVector value2) where TVector : ISimdVector => value1 | value2; + } - ref int value = ref MemoryMarshal.GetArrayDataReference(thisArray); - if (Vector512.IsHardwareAccelerated && (uint)count >= Vector512.Count) - { - for (; i < (uint)count - (Vector512.Count - 1u); i += (uint)Vector512.Count) - { - Vector512 result = ~Vector512.LoadUnsafe(ref value, i); - result.StoreUnsafe(ref value, i); - } - } - else if (Vector256.IsHardwareAccelerated && (uint)count >= Vector256.Count) - { - for (; i < (uint)count - (Vector256.Count - 1u); i += (uint)Vector256.Count) - { - Vector256 result = ~Vector256.LoadUnsafe(ref value, i); - result.StoreUnsafe(ref value, i); - } - } - else if (Vector128.IsHardwareAccelerated && (uint)count >= Vector128.Count) - { - for (; i < (uint)count - (Vector128.Count - 1u); i += (uint)Vector128.Count) - { - Vector128 result = ~Vector128.LoadUnsafe(ref value, i); - result.StoreUnsafe(ref value, i); - } - } + private struct XorBinaryOp : IBinaryOp + { + public static int Invoke(int value1, int value2) => value1 ^ value2; + public static TVector Invoke(TVector value1, TVector value2) where TVector : ISimdVector => value1 ^ value2; + } - for (; i < (uint)count; i++) - thisArray[i] = ~thisArray[i]; + private struct NotBinaryOp : IBinaryOp // not isn't binary, so second argument is just ignored + { + public static int Invoke(int value1, int _) => ~value1; + public static TVector Invoke(TVector value1, TVector value2) where TVector : ISimdVector => ~value1; + } - Done: - _version++; - return this; + private interface IBinaryOp + { + static abstract int Invoke(int value1, int value2); + static abstract TVector Invoke(TVector value1, TVector value2) where TVector : ISimdVector; } - /*========================================================================= - ** Shift all the bit values to right on count bits. The current instance is - ** updated and returned. - * - ** Exceptions: ArgumentOutOfRangeException if count < 0 - =========================================================================*/ + /// + /// Shifts all the bit values of the current to the right on bits. + /// + /// The number of shifts to make for each bit. + /// The current . + /// is less than zero. public BitArray RightShift(int count) { if (count <= 0) @@ -611,42 +466,37 @@ public BitArray RightShift(int count) int ints = GetInt32ArrayLengthFromBitLength(m_length); if (count < m_length) { - // We can not use Math.DivRem without taking a dependency on System.Runtime.Extensions - int fromIndex = Div32Rem(count, out int shiftCount); - Div32Rem(m_length, out int extraBits); + (int fromIndex, int shiftCount) = Math.DivRem(count, BitsPerInt32); + int extraBits = (int)((uint)m_length % BitsPerInt32); 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://learn.microsoft.com/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); - m_array[ints - 1] &= (int)mask; - } + // 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://learn.microsoft.com/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). + m_array[ints - 1] &= (int)ReverseIfBE(uint.MaxValue >> (BitsPerInt32 - extraBits)); + Array.Copy(m_array, fromIndex, m_array, 0, ints - fromIndex); toIndex = ints - fromIndex; } else { int lastIndex = ints - 1; - unchecked + + while (fromIndex < lastIndex) { - while (fromIndex < lastIndex) - { - uint right = (uint)m_array[fromIndex] >> shiftCount; - int left = m_array[++fromIndex] << (BitsPerInt32 - shiftCount); - m_array[toIndex++] = left | (int)right; - } - uint mask = uint.MaxValue >> (BitsPerInt32 - extraBits); - mask &= (uint)m_array[fromIndex]; - m_array[toIndex++] = (int)(mask >> shiftCount); + uint right = ReverseIfBE((uint)m_array[fromIndex]) >> shiftCount; + int left = ReverseIfBE(m_array[++fromIndex]) << BitsPerInt32 - shiftCount; + m_array[toIndex++] = ReverseIfBE(left | (int)right); } + + uint mask = uint.MaxValue >> (BitsPerInt32 - extraBits); + mask &= (uint)ReverseIfBE(m_array[fromIndex]); + m_array[toIndex++] = (int)ReverseIfBE(mask >> shiftCount); } } @@ -655,12 +505,12 @@ public BitArray RightShift(int count) return this; } - /*========================================================================= - ** Shift all the bit values to left on count bits. The current instance is - ** updated and returned. - * - ** Exceptions: ArgumentOutOfRangeException if count < 0 - =========================================================================*/ + /// + /// Shifts all the bit values of the current to the left on bits. + /// + /// The number of shifts to make for each bit. + /// The current . + /// is less than zero. public BitArray LeftShift(int count) { if (count <= 0) @@ -676,8 +526,7 @@ public BitArray LeftShift(int count) { int lastIndex = (m_length - 1) >> BitShiftPerInt32; // Divide by 32. - // We can not use Math.DivRem without taking a dependency on System.Runtime.Extensions - lengthToClear = Div32Rem(count, out int shiftCount); + (lengthToClear, int shiftCount) = Math.DivRem(count, BitsPerInt32); if (shiftCount == 0) { @@ -686,17 +535,15 @@ public BitArray LeftShift(int count) else { int fromindex = lastIndex - lengthToClear; - unchecked + + while (fromindex > 0) { - while (fromindex > 0) - { - int left = m_array[fromindex] << shiftCount; - uint right = (uint)m_array[--fromindex] >> (BitsPerInt32 - shiftCount); - m_array[lastIndex] = left | (int)right; - lastIndex--; - } - m_array[lastIndex] = m_array[fromindex] << shiftCount; + int left = ReverseIfBE(m_array[fromindex]) << shiftCount; + uint right = (uint)ReverseIfBE(m_array[--fromindex]) >> (BitsPerInt32 - shiftCount); + m_array[lastIndex] = ReverseIfBE(left | (int)right); + lastIndex--; } + m_array[lastIndex] = ReverseIfBE(ReverseIfBE(m_array[fromindex]) << shiftCount); } } else @@ -709,34 +556,38 @@ public BitArray LeftShift(int count) return this; } + /// + /// Gets or sets the number of elements in the . + /// + /// The number of elements in the . + /// The property is set to a value that is less than zero. public int Length { - get - { - return m_length; - } + get => m_length; set { ArgumentOutOfRangeException.ThrowIfNegative(value); + const int ShrinkThreshold = 256; + int newints = GetInt32ArrayLengthFromBitLength(value); - if (newints > m_array.Length || newints + _ShrinkThreshold < m_array.Length) + if (newints > m_array.Length || newints + ShrinkThreshold < m_array.Length) { - // grow or shrink (if wasting more than _ShrinkThreshold ints) + // Grow or shrink (if wasting more than ShrinkThreshold ints). Array.Resize(ref m_array, newints); } if (value > m_length) { - // clear high bit values in the last int + // Clear high bit values in the last int. int last = (m_length - 1) >> BitShiftPerInt32; - Div32Rem(m_length, out int bits); + int bits = (int)((uint)m_length % BitsPerInt32); if (bits > 0) { - m_array[last] &= (1 << bits) - 1; + m_array[last] &= ReverseIfBE(1 << bits) - 1; } - // clear remaining int values + // Clear remaining int values. m_array.AsSpan(last + 1, newints - last - 1).Clear(); } @@ -745,14 +596,16 @@ public int Length } } + /// public unsafe void CopyTo(Array array, int index) { ArgumentNullException.ThrowIfNull(array); - ArgumentOutOfRangeException.ThrowIfNegative(index); if (array.Rank != 1) + { throw new ArgumentException(SR.Arg_RankMultiDimNotSupported, nameof(array)); + } if (array is int[] intArray) { @@ -761,14 +614,21 @@ public unsafe void CopyTo(Array array, int index) throw new ArgumentException(SR.Argument_InvalidOffLen); } - int quotient = Div32Rem(m_length, out int extraBits); + (int quotient, int extraBits) = Math.DivRem(m_length, BitsPerInt32); - Array.Copy(m_array, 0, intArray, index, quotient); + if (BitConverter.IsLittleEndian) + { + Array.Copy(m_array, 0, intArray, index, quotient); + } + else + { + BinaryPrimitives.ReverseEndianness(m_array.AsSpan(0, quotient), intArray.AsSpan(index, quotient)); + } if (extraBits > 0) { - // the last int needs to be masked - intArray[index + quotient] = m_array[quotient] & unchecked((1 << extraBits) - 1); + // The last int needs to be masked. + intArray[index + quotient] = m_array[quotient] & ReverseIfBE((1 << extraBits) - 1); } } else if (array is byte[] byteArray) @@ -779,44 +639,11 @@ public unsafe void CopyTo(Array array, int index) throw new ArgumentException(SR.Argument_InvalidOffLen); } - // equivalent to m_length % BitsPerByte, since BitsPerByte is a power of 2 - uint extraBits = (uint)m_length & (BitsPerByte - 1); - if (extraBits > 0) - { - // last byte is not aligned, we will directly copy one less byte - arrayLength -= 1; - } - Span span = byteArray.AsSpan(index); - - int quotient = Div4Rem(arrayLength, out int remainder); - for (int i = 0; i < quotient; i++) - { - BinaryPrimitives.WriteInt32LittleEndian(span, m_array[i]); - span = span.Slice(4); - } - - if (extraBits > 0) + MemoryMarshal.AsBytes(m_array).Slice(0, arrayLength).CopyTo(span); + if ((uint)m_length % BitsPerByte is uint toMask && toMask != 0) { - Debug.Assert(span.Length > 0); - Debug.Assert(m_array.Length > quotient); - // mask the final byte - span[remainder] = (byte)((m_array[quotient] >> (remainder * 8)) & ((1 << (int)extraBits) - 1)); - } - - switch (remainder) - { - case 3: - span[2] = (byte)(m_array[quotient] >> 16); - goto case 2; - // fall through - case 2: - span[1] = (byte)(m_array[quotient] >> 8); - goto case 1; - // fall through - case 1: - span[0] = (byte)m_array[quotient]; - break; + span[^1] &= (byte)((1 << (int)toMask) - 1); } } else if (array is bool[] boolArray) @@ -828,8 +655,10 @@ public unsafe void CopyTo(Array array, int index) uint i = 0; - if (m_length < BitsPerInt32) + if (!BitConverter.IsLittleEndian || m_length < BitsPerInt32) + { goto LessThan32; + } // The mask used when shuffling a single int into Vector128/256/512. // On little endian machines, the lower 8 bits of int belong in the first byte, next lower 8 in the second and so on. @@ -866,7 +695,6 @@ public unsafe void CopyTo(Array array, int index) { Vector256 shuffleMask = Vector256.Create(lowerShuffleMask_CopyToBoolArray, upperShuffleMask_CopyToBoolArray); Vector256 bitMask = Vector256.Create(0x80402010_08040201).AsByte(); - //Internal.Console.WriteLine(bitMask); Vector256 ones = Vector256.Create((byte)1); fixed (bool* destination = &boolArray[index]) @@ -890,9 +718,7 @@ public unsafe void CopyTo(Array array, int index) Vector128 lowerShuffleMask = lowerShuffleMask_CopyToBoolArray; Vector128 upperShuffleMask = upperShuffleMask_CopyToBoolArray; Vector128 ones = Vector128.Create((byte)1); - Vector128 bitMask128 = BitConverter.IsLittleEndian ? - Vector128.Create(0x80402010_08040201).AsByte() : - Vector128.Create(0x01020408_10204080).AsByte(); + Vector128 bitMask128 = Vector128.Create(0x80402010_08040201).AsByte(); fixed (bool* destination = &boolArray[index]) { @@ -916,9 +742,7 @@ public unsafe void CopyTo(Array array, int index) else if (AdvSimd.Arm64.IsSupported) { Vector128 ones = Vector128.Create((byte)1); - Vector128 bitMask128 = BitConverter.IsLittleEndian ? - Vector128.Create(0x80402010_08040201).AsByte() : - Vector128.Create(0x01020408_10204080).AsByte(); + Vector128 bitMask128 = Vector128.Create(0x80402010_08040201).AsByte(); fixed (bool* destination = &boolArray[index]) { @@ -929,17 +753,13 @@ public unsafe void CopyTo(Array array, int index) // (TableVectorLookup could be an alternative - dotnet/runtime#1277) // Instead we use chained ZIP1/2 instructions: // (A0 is the byte containing LSB, A3 is the byte containing MSB) - // bits (on Big endian) - A3 A2 A1 A0 - // bits (Little endian) / Byte reversal - A0 A1 A2 A3 + // bits - A0 A1 A2 A3 // v1 = Vector128.Create - A0 A1 A2 A3 A0 A1 A2 A3 A0 A1 A2 A3 A0 A1 A2 A3 // v2 = ZipLow(v1, v1) - A0 A0 A1 A1 A2 A2 A3 A3 A0 A0 A1 A1 A2 A2 A3 A3 // v3 = ZipLow(v2, v2) - A0 A0 A0 A0 A1 A1 A1 A1 A2 A2 A2 A2 A3 A3 A3 A3 // shuffledLower = ZipLow(v3, v3) - A0 A0 A0 A0 A0 A0 A0 A0 A1 A1 A1 A1 A1 A1 A1 A1 // shuffledHigher = ZipHigh(v3, v3) - A2 A2 A2 A2 A2 A2 A2 A2 A3 A3 A3 A3 A3 A3 A3 A3 - if (!BitConverter.IsLittleEndian) - { - bits = BinaryPrimitives.ReverseEndianness(bits); - } + Vector128 vector = Vector128.Create(bits).AsByte(); vector = AdvSimd.Arm64.ZipLow(vector, vector); vector = AdvSimd.Arm64.ZipLow(vector, vector); @@ -960,8 +780,8 @@ public unsafe void CopyTo(Array array, int index) LessThan32: for (; i < (uint)m_length; i++) { - int elementIndex = Div32Rem((int)i, out int extraBits); - boolArray[(uint)index + i] = ((m_array[elementIndex] >> extraBits) & 0x00000001) != 0; + (uint elementIndex, uint extraBits) = Math.DivRem(i, BitsPerInt32); + boolArray[(uint)index + i] = ((m_array[elementIndex] >> (int)extraBits) & 0x00000001) != 0; } } else @@ -976,7 +796,7 @@ public unsafe void CopyTo(Array array, int index) /// true if every bit in the is set to true, or if is empty; otherwise, false. public bool HasAllSet() { - Div32Rem(m_length, out int extraBits); + int extraBits = (int)((uint)m_length % BitsPerInt32); int intCount = GetInt32ArrayLengthFromBitLength(m_length); if (extraBits != 0) { @@ -997,7 +817,7 @@ public bool HasAllSet() Debug.Assert(GetInt32ArrayLengthFromBitLength(m_length) > 0); Debug.Assert(intCount == GetInt32ArrayLengthFromBitLength(m_length) - 1); - int mask = (1 << extraBits) - 1; + int mask = ReverseIfBE(1 << extraBits) - 1; return (m_array[intCount] & mask) == mask; } @@ -1007,7 +827,7 @@ public bool HasAllSet() /// true if is not empty and at least one of its bit is set to true; otherwise, false. public bool HasAnySet() { - Div32Rem(m_length, out int extraBits); + int extraBits = (int)((uint)m_length % BitsPerInt32); int intCount = GetInt32ArrayLengthFromBitLength(m_length); if (extraBits != 0) { @@ -1027,19 +847,26 @@ public bool HasAnySet() Debug.Assert(GetInt32ArrayLengthFromBitLength(m_length) > 0); Debug.Assert(intCount == GetInt32ArrayLengthFromBitLength(m_length) - 1); - return (m_array[intCount] & (1 << extraBits) - 1) != 0; + return (m_array[intCount] & ReverseIfBE(1 << extraBits) - 1) != 0; } + /// Gets the number of elements contained in the . public int Count => m_length; + /// Gets an object that can be used to synchronize access to the . public object SyncRoot => this; + /// Gets a value indicating whether access to the is synchronized (thread safe). public bool IsSynchronized => false; + /// Gets a value indicating whether the is read-only. public bool IsReadOnly => false; + /// Creates a shallow copy of the . public object Clone() => new BitArray(this); + /// Returns an enumerator that iterates through the . + /// An IEnumerator for the entire . public IEnumerator GetEnumerator() => new BitArrayEnumeratorSimple(this); // XPerY=n means that n Xs can be stored in 1 Y. @@ -1074,44 +901,36 @@ private static int GetInt32ArrayLengthFromBitLength(int n) private static int GetInt32ArrayLengthFromByteLength(int n) { - Debug.Assert(n >= 0); // Due to sign extension, we don't need to special case for n == 0, since ((n - 1) >> 2) + 1 = 0 // This doesn't hold true for ((n - 1) / 4) + 1, which equals 1. + Debug.Assert(n >= 0); return (int)((uint)(n - 1 + (1 << BitShiftForBytesPerInt32)) >> BitShiftForBytesPerInt32); } - private static int GetByteArrayLengthFromBitLength(int n) + internal static int GetByteArrayLengthFromBitLength(int n) { - Debug.Assert(n >= 0); // Due to sign extension, we don't need to special case for n == 0, since ((n - 1) >> 3) + 1 = 0 // This doesn't hold true for ((n - 1) / 8) + 1, which equals 1. + Debug.Assert(n >= 0); return (int)((uint)(n - 1 + (1 << BitShiftPerByte)) >> BitShiftPerByte); } - private static int Div32Rem(int number, out int remainder) - { - uint quotient = (uint)number / 32; - remainder = number & (32 - 1); // equivalent to number % 32, since 32 is a power of 2 - return (int)quotient; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int ReverseIfBE(int value) => + BitConverter.IsLittleEndian ? value : BinaryPrimitives.ReverseEndianness(value); - private static int Div4Rem(int number, out int remainder) - { - uint quotient = (uint)number / 4; - remainder = number & (4 - 1); // equivalent to number % 4, since 4 is a power of 2 - return (int)quotient; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint ReverseIfBE(uint value) => + BitConverter.IsLittleEndian ? value : BinaryPrimitives.ReverseEndianness(value); - private static void ThrowArgumentOutOfRangeException(int index) - { + private static void ThrowArgumentOutOfRangeException(int index) => throw new ArgumentOutOfRangeException(nameof(index), index, SR.ArgumentOutOfRange_IndexMustBeLess); - } private sealed class BitArrayEnumeratorSimple : IEnumerator, ICloneable { private readonly BitArray _bitArray; - private int _index; private readonly int _version; + private int _index; private bool _currentElement; internal BitArrayEnumeratorSimple(BitArray bitArray) @@ -1136,11 +955,8 @@ public bool MoveNext() _currentElement = _bitArray.Get(_index); return true; } - else - { - _index = _bitArray.m_length; - } + _index = _bitArray.m_length; return false; } diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/CollectionsMarshal.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/CollectionsMarshal.cs index 9c372574ed0dec..d3286cd1e3fdcb 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/CollectionsMarshal.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/CollectionsMarshal.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Runtime.CompilerServices; @@ -41,6 +42,30 @@ public static Span AsSpan(List? list) return span; } + /// + /// Get a view over a 's data. + /// + /// The whose backing storage should be viewed. + /// + /// + /// The may have more capacity than is required to store the number of bits represented by + /// . The returned span's will be the smallest number + /// of bytes capable of representing that length. If the 's length is not evenly divisible by + /// 8, the last byte of the span may contain extraneous bits that do not represent elements in the . + /// These may be ignored. + /// + /// + /// The length of the should not be changed while the resulting + /// is in use. After such a change, the span may no longer refer to the 's backing storage. + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span AsBytes(BitArray? array) => + array is null ? default : + MemoryMarshal + .AsBytes((Span)array.m_array) + [..BitArray.GetByteArrayLengthFromBitLength(array.Length)]; + /// /// Gets either a ref to a in the or a ref null if it does not exist in the . /// diff --git a/src/libraries/System.Runtime.InteropServices/ref/System.Runtime.InteropServices.cs b/src/libraries/System.Runtime.InteropServices/ref/System.Runtime.InteropServices.cs index e00a6de33c437f..91534aa6f11c5e 100644 --- a/src/libraries/System.Runtime.InteropServices/ref/System.Runtime.InteropServices.cs +++ b/src/libraries/System.Runtime.InteropServices/ref/System.Runtime.InteropServices.cs @@ -679,6 +679,7 @@ public CoClassAttribute(System.Type coClass) { } } public static partial class CollectionsMarshal { + public static System.Span AsBytes(System.Collections.BitArray? array) { throw null; } public static System.Span AsSpan(System.Collections.Generic.List? list) { throw null; } public static ref TValue GetValueRefOrNullRef(System.Collections.Generic.Dictionary dictionary, TKey key) where TKey : notnull { throw null; } public static ref TValue GetValueRefOrNullRef(System.Collections.Generic.Dictionary.AlternateLookup dictionary, TAlternateKey key) where TKey : notnull where TAlternateKey : notnull, allows ref struct { throw null; } From c04e674ac5413b320bbeba0af464a7a9cdd9b904 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Sun, 8 Jun 2025 22:00:01 -0400 Subject: [PATCH 3/5] Update to use a byte[] backing array --- .../src/System/Collections/BitArray.cs | 478 +++++++++--------- .../InteropServices/CollectionsMarshal.cs | 4 +- 2 files changed, 226 insertions(+), 256 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Collections/BitArray.cs b/src/libraries/System.Private.CoreLib/src/System/Collections/BitArray.cs index ad6c94c71a0b2b..dc18b30f9392d0 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Collections/BitArray.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Collections/BitArray.cs @@ -3,6 +3,7 @@ using System.Buffers.Binary; using System.Diagnostics; +using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; @@ -16,14 +17,27 @@ namespace System.Collections /// indicates that the bit is on (1) and indicates /// the bit is off (0). /// - [Serializable] - [TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")] + [Serializable] // leaving it serializable, but no longer compatible with pre-.NET 10 serialization format public sealed class BitArray : ICollection, ICloneable { - /// Bit array. Always little endian, even on big endian platforms. - internal int[] m_array; // Do not rename (binary serialization) - private int m_length; // Do not rename (binary serialization) - private int _version; // Do not rename (binary serialization) + /// sizeof(int) * 8 + private const int BitsPerInt32 = 32; + /// sizeof(byte) * 8 + private const int BitsPerByte = 8; + + /// The array of bytes used to store bits. + /// + /// The array is allocated to hold enough bytes to store the specified number of bits, rounded up to the nearest multiple + /// of sizeof(int). The last four bytes that might contain valid bits are always kept in a state where the unused bits + /// are cleared, such that AsBytes and CopyTo operations will not show any set bits that are not actually set. + /// + internal byte[] _array; + + /// The number of bits in the array. + private int _bitLength; + + /// Version number incremented on mutation, used to invalidate enumerators. + private int _version; /// /// Initializes a new instance of the class that can hold the specified @@ -47,19 +61,22 @@ public BitArray(int length, bool defaultValue) { ArgumentOutOfRangeException.ThrowIfNegative(length); - m_array = new int[GetInt32ArrayLengthFromBitLength(length)]; - m_length = length; + _array = AllocateByteArray(length); + _bitLength = length; if (defaultValue) { - Array.Fill(m_array, -1); + Array.Fill(_array, (byte)0xFF); + ClearHighExtraBits(); + } + } - // Clear high bit values in the last int. - int extraBits = (int)((uint)length % BitsPerInt32); - if (extraBits != 0) - { - m_array[^1] = ReverseIfBE((1 << extraBits) - 1); - } + private void ClearHighExtraBits() + { + (uint index, uint extraBits) = Math.DivRem((uint)_bitLength, BitsPerInt32); + if (extraBits != 0) + { + MemoryMarshal.Cast((Span)_array)[(int)index] &= ReverseIfBE((1 << (int)extraBits) - 1); } } @@ -78,10 +95,10 @@ public BitArray(byte[] bytes) throw new ArgumentException(SR.Format(SR.Argument_ArrayTooLarge, BitsPerByte), nameof(bytes)); } - m_array = new int[GetInt32ArrayLengthFromByteLength(bytes.Length)]; - m_length = bytes.Length * BitsPerByte; + _bitLength = bytes.Length * BitsPerByte; + _array = AllocateByteArray(_bitLength); - bytes.AsSpan().CopyTo(MemoryMarshal.AsBytes((Span)m_array)); + Array.Copy(bytes, _array, bytes.Length); } /// @@ -94,20 +111,21 @@ public BitArray(bool[] values) { ArgumentNullException.ThrowIfNull(values); - m_array = new int[GetInt32ArrayLengthFromBitLength(values.Length)]; - m_length = values.Length; + _array = AllocateByteArray(values.Length); + _bitLength = values.Length; uint i = 0; if (!BitConverter.IsLittleEndian || values.Length < Vector256.Count) { - goto LessThan32; + goto Remainder; } // Comparing with 1s would get rid of the final negation, however this would not work for some CLR bools // (true for any non-zero values, false for 0) - any values between 2-255 will be interpreted as false. // Instead, We compare with zeroes (== false) then negate the result to ensure compatibility. + ref byte arrayRef = ref MemoryMarshal.GetArrayDataReference(_array); ref byte value = ref Unsafe.As(ref MemoryMarshal.GetArrayDataReference(values)); if (Vector512.IsHardwareAccelerated) { @@ -117,8 +135,7 @@ public BitArray(bool[] values) Vector512 isFalse = Vector512.Equals(vector, Vector512.Zero); ulong result = isFalse.ExtractMostSignificantBits(); - m_array[i / 32u] = (int)(~result & 0x00000000FFFFFFFF); - m_array[(i / 32u) + 1] = (int)((~result >> 32) & 0x00000000FFFFFFFF); + Unsafe.WriteUnaligned(ref Unsafe.Add(ref arrayRef, sizeof(ulong) * (i / 64u)), ~result); } } else if (Vector256.IsHardwareAccelerated) @@ -129,7 +146,7 @@ public BitArray(bool[] values) Vector256 isFalse = Vector256.Equals(vector, Vector256.Zero); uint result = isFalse.ExtractMostSignificantBits(); - m_array[i / 32u] = (int)(~result); + Unsafe.WriteUnaligned(ref Unsafe.Add(ref arrayRef, sizeof(uint) * (i / 32u)), ~result); } } else if (Vector128.IsHardwareAccelerated) @@ -144,17 +161,19 @@ public BitArray(bool[] values) Vector128 upperIsFalse = Vector128.Equals(upperVector, Vector128.Zero); uint upperResult = upperIsFalse.ExtractMostSignificantBits(); - m_array[i / 32u] = (int)(~((upperResult << 16) | lowerResult)); + Unsafe.WriteUnaligned( + ref Unsafe.Add(ref arrayRef, sizeof(uint) * (i / 32u)), + ~((upperResult << 16) | lowerResult)); } } - LessThan32: + Remainder: for (; i < (uint)values.Length; i++) { if (values[i]) { - (uint elementIndex, uint extraBits) = Math.DivRem(i, BitsPerInt32); - m_array[elementIndex] |= ReverseIfBE(1 << (int)extraBits); + (uint byteIndex, uint bitOffset) = Math.DivRem(i, BitsPerByte); + _array[byteIndex] |= (byte)(1 << (int)bitOffset); } } } @@ -180,17 +199,17 @@ public BitArray(int[] values) throw new ArgumentException(SR.Format(SR.Argument_ArrayTooLarge, BitsPerInt32), nameof(values)); } - m_array = new int[values.Length]; + _bitLength = values.Length * BitsPerInt32; + _array = AllocateByteArray(_bitLength); + if (BitConverter.IsLittleEndian) { - Array.Copy(values, m_array, values.Length); + MemoryMarshal.AsBytes(values.AsSpan()).CopyTo(_array); } else { - BinaryPrimitives.ReverseEndianness(values, m_array); + BinaryPrimitives.ReverseEndianness(values, MemoryMarshal.Cast((Span)_array)); } - - m_length = values.Length * BitsPerInt32; } /// @@ -202,13 +221,10 @@ public BitArray(BitArray bits) { ArgumentNullException.ThrowIfNull(bits); - int arrayLength = GetInt32ArrayLengthFromBitLength(bits.m_length); + _bitLength = bits._bitLength; + _array = AllocateByteArray(_bitLength); - m_array = new int[arrayLength]; - Debug.Assert(bits.m_array.Length <= arrayLength); - - Array.Copy(bits.m_array, m_array, arrayLength); - m_length = bits.m_length; + Array.Copy(bits._array, _array, _array.Length); } /// @@ -235,12 +251,13 @@ public bool this[int index] [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Get(int index) { - if ((uint)index >= (uint)m_length) + if ((uint)index >= (uint)_bitLength) { ThrowArgumentOutOfRangeException(index); } - return (ReverseIfBE(m_array[index >> BitShiftPerInt32]) & (1 << index)) != 0; + (uint byteIndex, uint bitOffset) = Math.DivRem((uint)index, BitsPerByte); + return ((_array[byteIndex]) & (1 << (int)bitOffset)) != 0; } /// @@ -253,22 +270,22 @@ public bool Get(int index) [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Set(int index, bool value) { - if ((uint)index >= (uint)m_length) + if ((uint)index >= (uint)_bitLength) { ThrowArgumentOutOfRangeException(index); } - int bitMask = ReverseIfBE(1 << index); - - ref int segment = ref m_array[index >> BitShiftPerInt32]; + (uint byteIndex, uint bitOffset) = Math.DivRem((uint)index, BitsPerByte); + ref byte segment = ref _array[byteIndex]; + byte bitMask = (byte)(1 << (int)bitOffset); if (value) { segment |= bitMask; } else { - segment &= ~bitMask; + segment &= (byte)~bitMask; } _version++; @@ -280,28 +297,19 @@ public void Set(int index, bool value) /// The Boolean value to assign to all bits. public void SetAll(bool value) { - int arrayLength = GetInt32ArrayLengthFromBitLength(Length); - Span span = m_array.AsSpan(0, arrayLength); if (value) { - span.Fill(-1); - - // Clear high bit values in the last int. - int extraBits = (int)((uint)m_length % BitsPerInt32); - if (extraBits != 0) - { - span[^1] &= ReverseIfBE((1 << extraBits) - 1); - } + _array.AsSpan(0, GetByteArrayLengthFromBitLength(_bitLength)).Fill(0xFF); + ClearHighExtraBits(); } else { - span.Clear(); + MemoryMarshal.Cast((Span)_array).Slice(0, GetInt32ArrayLengthFromBitLength(_bitLength)).Clear(); } _version++; } - /// /// Performs the bitwise AND operation between the elements of the current object and the /// corresponding elements in the specified array. The current object will be modified to @@ -340,7 +348,12 @@ public void SetAll(bool value) /// and elements set to false are changed to true. /// /// The current instance with inverted bit values. - public BitArray Not() => Invoke(this); // argument is ignored + public BitArray Not() + { + Invoke(this); // argument is ignored + ClearHighExtraBits(); // applying ~ to last Int32 may set extra bits we're trying to keep clear + return this; + } /// Provides the implementation for , , etc. private BitArray Invoke(BitArray value) where TBinaryOp : struct, IBinaryOp @@ -353,9 +366,9 @@ private BitArray Invoke(BitArray value) where TBinaryOp : struct, IBi // 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. - int[] thisArray = m_array; - int[] valueArray = value.m_array; - int count = GetInt32ArrayLengthFromBitLength(Length); + byte[] thisArray = _array; + byte[] valueArray = value._array; + int count = GetByteArrayLengthFromBitLength(Length); if (Length != value.Length || (uint)count > (uint)thisArray.Length || @@ -364,86 +377,83 @@ private BitArray Invoke(BitArray value) where TBinaryOp : struct, IBi throw new ArgumentException(SR.Arg_ArrayLengthsDiffer); } - // Unroll loop for count less than Vector256 size. - switch (count) - { - case 7: thisArray[6] = TBinaryOp.Invoke(thisArray[6], valueArray[6]); goto case 6; - case 6: thisArray[5] = TBinaryOp.Invoke(thisArray[5], valueArray[5]); goto case 5; - case 5: thisArray[4] = TBinaryOp.Invoke(thisArray[4], valueArray[4]); goto case 4; - case 4: thisArray[3] = TBinaryOp.Invoke(thisArray[3], valueArray[3]); goto case 3; - case 3: thisArray[2] = TBinaryOp.Invoke(thisArray[2], valueArray[2]); goto case 2; - case 2: thisArray[1] = TBinaryOp.Invoke(thisArray[1], valueArray[1]); goto case 1; - case 1: thisArray[0] = TBinaryOp.Invoke(thisArray[0], valueArray[0]); goto Done; - case 0: goto Done; - } - - uint i = 0; + int i = 0; if (Vector512.IsHardwareAccelerated) { - Apply>(count, ref i, thisArray, valueArray); + i = Apply>(count, thisArray, valueArray); } else if (Vector256.IsHardwareAccelerated) { - Apply>(count, ref i, thisArray, valueArray); + i = Apply>(count, thisArray, valueArray); } else if (Vector128.IsHardwareAccelerated) { - Apply>(count, ref i, thisArray, valueArray); + i = Apply>(count, thisArray, valueArray); } - for (; i < (uint)count; i++) + // Process remaining. + if (i != count) { - thisArray[i] = TBinaryOp.Invoke(thisArray[i], valueArray[i]); + int roundedBytesRemaining = RoundUpToMultipleSizeOfInt32(count - i); + Span thisRemaining = MemoryMarshal.Cast(((Span)thisArray).Slice(i, roundedBytesRemaining)); + Span valueRemaining = MemoryMarshal.Cast(((Span)valueArray).Slice(i, roundedBytesRemaining)); + for (i = 0; i < thisRemaining.Length; i++) + { + thisRemaining[i] = TBinaryOp.Invoke(thisRemaining[i], valueRemaining[i]); + } } - Done: _version++; return this; [MethodImpl(MethodImplOptions.AggressiveInlining)] - static void Apply(int count, ref uint i, int[] thisArray, int[] valueArray) - where TVector : ISimdVector + static int Apply(int count, byte[] thisArray, byte[] valueArray) + where TVector : ISimdVector { - ref int left = ref MemoryMarshal.GetArrayDataReference(thisArray); - ref int right = ref MemoryMarshal.GetArrayDataReference(valueArray); + ref byte left = ref MemoryMarshal.GetArrayDataReference(thisArray); + ref byte right = ref MemoryMarshal.GetArrayDataReference(valueArray); + + int i; - for (; i < (uint)count - (TVector.ElementCount - 1u); i += (uint)TVector.ElementCount) + for (i = 0; i <= count - TVector.ElementCount; i += TVector.ElementCount) { - TVector result = TBinaryOp.Invoke(TVector.LoadUnsafe(ref left, i), TVector.LoadUnsafe(ref right, i)); - result.StoreUnsafe(ref left, i); + TVector result = TBinaryOp.Invoke(TVector.LoadUnsafe(ref left, (uint)i), TVector.LoadUnsafe(ref right, (uint)i)); + result.StoreUnsafe(ref left, (uint)i); } + + return i; } } private struct AndBinaryOp : IBinaryOp { public static int Invoke(int value1, int value2) => value1 & value2; - public static TVector Invoke(TVector value1, TVector value2) where TVector : ISimdVector => value1 & value2; + public static TVector Invoke(TVector value1, TVector value2) where TVector : ISimdVector => value1 & value2; } private struct OrBinaryOp : IBinaryOp { public static int Invoke(int value1, int value2) => value1 | value2; - public static TVector Invoke(TVector value1, TVector value2) where TVector : ISimdVector => value1 | value2; + public static TVector Invoke(TVector value1, TVector value2) where TVector : ISimdVector => value1 | value2; } private struct XorBinaryOp : IBinaryOp { public static int Invoke(int value1, int value2) => value1 ^ value2; - public static TVector Invoke(TVector value1, TVector value2) where TVector : ISimdVector => value1 ^ value2; + public static TVector Invoke(TVector value1, TVector value2) where TVector : ISimdVector => value1 ^ value2; } private struct NotBinaryOp : IBinaryOp // not isn't binary, so second argument is just ignored { public static int Invoke(int value1, int _) => ~value1; - public static TVector Invoke(TVector value1, TVector value2) where TVector : ISimdVector => ~value1; + public static TVector Invoke(TVector value1, TVector value2) where TVector : ISimdVector => ~value1; } private interface IBinaryOp { static abstract int Invoke(int value1, int value2); - static abstract TVector Invoke(TVector value1, TVector value2) where TVector : ISimdVector; + static abstract TVector Invoke(TVector value1, TVector value2) where TVector : ISimdVector; } /// @@ -462,12 +472,15 @@ public BitArray RightShift(int count) return this; } + Span array = MemoryMarshal.Cast((Span)_array); + int toIndex = 0; - int ints = GetInt32ArrayLengthFromBitLength(m_length); - if (count < m_length) + int ints = GetInt32ArrayLengthFromBitLength(_bitLength); + if (count < _bitLength) { - (int fromIndex, int shiftCount) = Math.DivRem(count, BitsPerInt32); - int extraBits = (int)((uint)m_length % BitsPerInt32); + // We can not use Math.DivRem without taking a dependency on System.Runtime.Extensions + (int fromIndex, int shiftCount) = Math.DivRem((int)count, 32); + int extraBits = (int)((uint)_bitLength % 32); if (shiftCount == 0) { // Cannot use `(1u << extraBits) - 1u` as the mask @@ -478,10 +491,11 @@ public BitArray RightShift(int count) // https://learn.microsoft.com/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). - m_array[ints - 1] &= (int)ReverseIfBE(uint.MaxValue >> (BitsPerInt32 - extraBits)); + uint mask = uint.MaxValue >> (BitsPerInt32 - extraBits); + array[ints - 1] &= (int)mask; - Array.Copy(m_array, fromIndex, m_array, 0, ints - fromIndex); - toIndex = ints - fromIndex; + array.Slice((int)fromIndex, ints - fromIndex).CopyTo(array); + toIndex = ints - (int)fromIndex; } else { @@ -489,18 +503,18 @@ public BitArray RightShift(int count) while (fromIndex < lastIndex) { - uint right = ReverseIfBE((uint)m_array[fromIndex]) >> shiftCount; - int left = ReverseIfBE(m_array[++fromIndex]) << BitsPerInt32 - shiftCount; - m_array[toIndex++] = ReverseIfBE(left | (int)right); + uint right = (uint)array[fromIndex] >> shiftCount; + int left = array[++fromIndex] << (BitsPerInt32 - shiftCount); + array[toIndex++] = left | (int)right; } uint mask = uint.MaxValue >> (BitsPerInt32 - extraBits); - mask &= (uint)ReverseIfBE(m_array[fromIndex]); - m_array[toIndex++] = (int)ReverseIfBE(mask >> shiftCount); + mask &= (uint)array[fromIndex]; + array[toIndex++] = (int)(mask >> shiftCount); } } - m_array.AsSpan(toIndex, ints - toIndex).Clear(); + array.Slice(toIndex, ints - toIndex).Clear(); _version++; return this; } @@ -521,16 +535,18 @@ public BitArray LeftShift(int count) return this; } + Span span = MemoryMarshal.Cast((Span)_array); + int lengthToClear; - if (count < m_length) + if (count < _bitLength) { - int lastIndex = (m_length - 1) >> BitShiftPerInt32; // Divide by 32. + int lastIndex = (int)((uint)(_bitLength - 1) / BitsPerInt32); (lengthToClear, int shiftCount) = Math.DivRem(count, BitsPerInt32); if (shiftCount == 0) { - Array.Copy(m_array, 0, m_array, lengthToClear, lastIndex + 1 - lengthToClear); + span.Slice(0, lastIndex + 1 - lengthToClear).CopyTo(span.Slice(lengthToClear)); } else { @@ -538,20 +554,20 @@ public BitArray LeftShift(int count) while (fromindex > 0) { - int left = ReverseIfBE(m_array[fromindex]) << shiftCount; - uint right = (uint)ReverseIfBE(m_array[--fromindex]) >> (BitsPerInt32 - shiftCount); - m_array[lastIndex] = ReverseIfBE(left | (int)right); + int left = ReverseIfBE(span[fromindex]) << shiftCount; + uint right = (uint)ReverseIfBE(span[--fromindex]) >> (BitsPerInt32 - shiftCount); + span[lastIndex] = ReverseIfBE(left | (int)right); lastIndex--; } - m_array[lastIndex] = ReverseIfBE(ReverseIfBE(m_array[fromindex]) << shiftCount); + span[lastIndex] = ReverseIfBE(ReverseIfBE(span[fromindex]) << shiftCount); } } else { - lengthToClear = GetInt32ArrayLengthFromBitLength(m_length); // Clear all + lengthToClear = GetInt32ArrayLengthFromBitLength(_bitLength); // Clear all } - m_array.AsSpan(0, lengthToClear).Clear(); + span.Slice(0, lengthToClear).Clear(); _version++; return this; } @@ -563,35 +579,37 @@ public BitArray LeftShift(int count) /// The property is set to a value that is less than zero. public int Length { - get => m_length; + get => _bitLength; set { ArgumentOutOfRangeException.ThrowIfNegative(value); - const int ShrinkThreshold = 256; - - int newints = GetInt32ArrayLengthFromBitLength(value); - if (newints > m_array.Length || newints + ShrinkThreshold < m_array.Length) + int newByteLength = GetAlignedByteArrayLength(value); + if (newByteLength > _array.Length) { - // Grow or shrink (if wasting more than ShrinkThreshold ints). - Array.Resize(ref m_array, newints); + Array.Resize(ref _array, newByteLength); } - - if (value > m_length) + else { - // Clear high bit values in the last int. - int last = (m_length - 1) >> BitShiftPerInt32; - int bits = (int)((uint)m_length % BitsPerInt32); - if (bits > 0) + int currentByteLength = GetByteArrayLengthFromBitLength(_bitLength); + if (newByteLength > currentByteLength) { - m_array[last] &= ReverseIfBE(1 << bits) - 1; + _array.AsSpan(currentByteLength).Clear(); + } + else + { + // If we'll be shrinking by a significant amount, re-allocate to avoid wasting too much space. + const int ShrinkThreshold = 1024; + if (newByteLength < _array.Length - ShrinkThreshold) + { + Array.Resize(ref _array, newByteLength); + } } - - // Clear remaining int values. - m_array.AsSpan(last + 1, newints - last - 1).Clear(); } - m_length = value; + _bitLength = value; + ClearHighExtraBits(); + _version++; } } @@ -609,64 +627,57 @@ public unsafe void CopyTo(Array array, int index) if (array is int[] intArray) { - if (array.Length - index < GetInt32ArrayLengthFromBitLength(m_length)) + int int32Length = GetInt32ArrayLengthFromBitLength(_bitLength); + + if (array.Length - index < int32Length) { throw new ArgumentException(SR.Argument_InvalidOffLen); } - (int quotient, int extraBits) = Math.DivRem(m_length, BitsPerInt32); - + Span source = MemoryMarshal.Cast((Span)_array).Slice(0, int32Length); if (BitConverter.IsLittleEndian) { - Array.Copy(m_array, 0, intArray, index, quotient); + source.CopyTo(intArray.AsSpan(index)); } else { - BinaryPrimitives.ReverseEndianness(m_array.AsSpan(0, quotient), intArray.AsSpan(index, quotient)); - } - - if (extraBits > 0) - { - // The last int needs to be masked. - intArray[index + quotient] = m_array[quotient] & ReverseIfBE((1 << extraBits) - 1); + BinaryPrimitives.ReverseEndianness(source, intArray.AsSpan(index)); } } else if (array is byte[] byteArray) { - int arrayLength = GetByteArrayLengthFromBitLength(m_length); - if ((array.Length - index) < arrayLength) + int byteLength = GetByteArrayLengthFromBitLength(_bitLength); + + if ((array.Length - index) < byteLength) { throw new ArgumentException(SR.Argument_InvalidOffLen); } - Span span = byteArray.AsSpan(index); - MemoryMarshal.AsBytes(m_array).Slice(0, arrayLength).CopyTo(span); - if ((uint)m_length % BitsPerByte is uint toMask && toMask != 0) - { - span[^1] &= (byte)((1 << (int)toMask) - 1); - } + _array.AsSpan(0, byteLength).CopyTo(byteArray.AsSpan(index)); } else if (array is bool[] boolArray) { - if (array.Length - index < m_length) + if (boolArray.Length - index < _bitLength) { throw new ArgumentException(SR.Argument_InvalidOffLen); } uint i = 0; - if (!BitConverter.IsLittleEndian || m_length < BitsPerInt32) + if (!BitConverter.IsLittleEndian || _bitLength < BitsPerInt32) { - goto LessThan32; + goto Remainder; } + Span in32Span = MemoryMarshal.Cast((Span)_array); + // The mask used when shuffling a single int into Vector128/256/512. // On little endian machines, the lower 8 bits of int belong in the first byte, next lower 8 in the second and so on. // We place the bytes that contain the bits to its respective byte so that we can mask out only the relevant bits later. Vector128 lowerShuffleMask_CopyToBoolArray = Vector128.Create(0, 0x01010101_01010101).AsByte(); Vector128 upperShuffleMask_CopyToBoolArray = Vector128.Create(0x02020202_02020202, 0x03030303_03030303).AsByte(); - if (Avx512BW.IsSupported && (uint)m_length >= Vector512.Count) + if (Avx512BW.IsSupported && (uint)_bitLength >= Vector512.Count) { Vector256 upperShuffleMask_CopyToBoolArray256 = Vector256.Create(0x04040404_04040404, 0x05050505_05050505, 0x06060606_06060606, 0x07070707_07070707).AsByte(); @@ -677,9 +688,9 @@ public unsafe void CopyTo(Array array, int index) fixed (bool* destination = &boolArray[index]) { - for (; (i + Vector512.Count) <= (uint)m_length; i += (uint)Vector512.Count) + for (; (i + Vector512.Count) <= (uint)_bitLength; i += (uint)Vector512.Count) { - ulong bits = (ulong)(uint)m_array[i / (uint)BitsPerInt32] + ((ulong)m_array[(i / (uint)BitsPerInt32) + 1] << BitsPerInt32); + ulong bits = (ulong)(uint)in32Span[(int)(i / (uint)BitsPerInt32)] + ((ulong)in32Span[(int)(i / (uint)BitsPerInt32) + 1] << BitsPerInt32); Vector512 scalar = Vector512.Create(bits); Vector512 shuffled = Avx512BW.Shuffle(scalar.AsByte(), shuffleMask); Vector512 extracted = Avx512F.And(shuffled, bitMask); @@ -691,7 +702,7 @@ public unsafe void CopyTo(Array array, int index) } } } - else if (Avx2.IsSupported && (uint)m_length >= Vector256.Count) + else if (Avx2.IsSupported && (uint)_bitLength >= Vector256.Count) { Vector256 shuffleMask = Vector256.Create(lowerShuffleMask_CopyToBoolArray, upperShuffleMask_CopyToBoolArray); Vector256 bitMask = Vector256.Create(0x80402010_08040201).AsByte(); @@ -699,9 +710,9 @@ public unsafe void CopyTo(Array array, int index) fixed (bool* destination = &boolArray[index]) { - for (; (i + Vector256.Count) <= (uint)m_length; i += (uint)Vector256.Count) + for (; (i + Vector256.Count) <= (uint)_bitLength; i += (uint)Vector256.Count) { - int bits = m_array[i / (uint)BitsPerInt32]; + int bits = in32Span[(int)(i / (uint)BitsPerInt32)]; Vector256 scalar = Vector256.Create(bits); Vector256 shuffled = Avx2.Shuffle(scalar.AsByte(), shuffleMask); Vector256 extracted = Avx2.And(shuffled, bitMask); @@ -713,7 +724,7 @@ public unsafe void CopyTo(Array array, int index) } } } - else if (Ssse3.IsSupported && ((uint)m_length >= Vector128.Count * 2u)) + else if (Ssse3.IsSupported && ((uint)_bitLength >= Vector128.Count * 2u)) { Vector128 lowerShuffleMask = lowerShuffleMask_CopyToBoolArray; Vector128 upperShuffleMask = upperShuffleMask_CopyToBoolArray; @@ -722,9 +733,9 @@ public unsafe void CopyTo(Array array, int index) fixed (bool* destination = &boolArray[index]) { - for (; (i + Vector128.Count * 2u) <= (uint)m_length; i += (uint)Vector128.Count * 2u) + for (; (i + Vector128.Count * 2u) <= (uint)_bitLength; i += (uint)Vector128.Count * 2u) { - int bits = m_array[i / (uint)BitsPerInt32]; + int bits = in32Span[(int)(i / (uint)BitsPerInt32)]; Vector128 scalar = Vector128.CreateScalarUnsafe(bits); Vector128 shuffledLower = Ssse3.Shuffle(scalar.AsByte(), lowerShuffleMask); @@ -746,9 +757,10 @@ public unsafe void CopyTo(Array array, int index) fixed (bool* destination = &boolArray[index]) { - for (; (i + Vector128.Count * 2u) <= (uint)m_length; i += (uint)Vector128.Count * 2u) + for (; (i + Vector128.Count * 2u) <= (uint)_bitLength; i += (uint)Vector128.Count * 2u) { - int bits = m_array[i / (uint)BitsPerInt32]; + int bits = in32Span[(int)(i / (uint)BitsPerInt32)]; + // Same logic as SSSE3 path, except we do not have Shuffle instruction. // (TableVectorLookup could be an alternative - dotnet/runtime#1277) // Instead we use chained ZIP1/2 instructions: @@ -777,11 +789,11 @@ public unsafe void CopyTo(Array array, int index) } } - LessThan32: - for (; i < (uint)m_length; i++) + Remainder: + for (; i < (uint)_bitLength; i++) { - (uint elementIndex, uint extraBits) = Math.DivRem(i, BitsPerInt32); - boolArray[(uint)index + i] = ((m_array[elementIndex] >> (int)extraBits) & 0x00000001) != 0; + (uint byteIndex, uint extraBits) = Math.DivRem(i, BitsPerByte); + boolArray[(uint)index + i] = (_array[byteIndex] & (1 << (int)extraBits)) != 0; } } else @@ -796,15 +808,14 @@ public unsafe void CopyTo(Array array, int index) /// true if every bit in the is set to true, or if is empty; otherwise, false. public bool HasAllSet() { - int extraBits = (int)((uint)m_length % BitsPerInt32); - int intCount = GetInt32ArrayLengthFromBitLength(m_length); + int extraBits = (int)((uint)_bitLength % BitsPerByte); + int byteCount = GetByteArrayLengthFromBitLength(_bitLength); if (extraBits != 0) { - intCount--; + byteCount--; } - const int AllSetBits = -1; // 0xFF_FF_FF_FF - if (m_array.AsSpan(0, intCount).ContainsAnyExcept(AllSetBits)) + if (_array.AsSpan(0, byteCount).ContainsAnyExcept((byte)0xFF)) { return false; } @@ -814,44 +825,19 @@ public bool HasAllSet() return true; } - Debug.Assert(GetInt32ArrayLengthFromBitLength(m_length) > 0); - Debug.Assert(intCount == GetInt32ArrayLengthFromBitLength(m_length) - 1); - - int mask = ReverseIfBE(1 << extraBits) - 1; - return (m_array[intCount] & mask) == mask; + byte mask = (byte)((1 << extraBits) - 1); + return (_array[byteCount] & mask) == mask; } /// /// Determines whether any bit in the is set to true. /// /// true if is not empty and at least one of its bit is set to true; otherwise, false. - public bool HasAnySet() - { - int extraBits = (int)((uint)m_length % BitsPerInt32); - int intCount = GetInt32ArrayLengthFromBitLength(m_length); - if (extraBits != 0) - { - intCount--; - } - - if (m_array.AsSpan(0, intCount).ContainsAnyExcept(0)) - { - return true; - } - - if (extraBits == 0) - { - return false; - } - - Debug.Assert(GetInt32ArrayLengthFromBitLength(m_length) > 0); - Debug.Assert(intCount == GetInt32ArrayLengthFromBitLength(m_length) - 1); - - return (m_array[intCount] & ReverseIfBE(1 << extraBits) - 1) != 0; - } + public bool HasAnySet() => + _array.AsSpan(0, GetByteArrayLengthFromBitLength(_bitLength)).ContainsAnyExcept((byte)0); /// Gets the number of elements contained in the . - public int Count => m_length; + public int Count => _bitLength; /// Gets an object that can be used to synchronize access to the . public object SyncRoot => this; @@ -869,69 +855,55 @@ public bool HasAnySet() /// An IEnumerator for the entire . public IEnumerator GetEnumerator() => new BitArrayEnumeratorSimple(this); - // XPerY=n means that n Xs can be stored in 1 Y. - private const int BitsPerInt32 = 32; - private const int BitsPerByte = 8; - - private const int BitShiftPerInt32 = 5; - private const int BitShiftPerByte = 3; - private const int BitShiftForBytesPerInt32 = 2; - - /// - /// Used for conversion between different representations of bit array. - /// Returns (n + (32 - 1)) / 32, rearranged to avoid arithmetic overflow. - /// For example, in the bit to int case, the straightforward calc would - /// be (n + 31) / 32, but that would cause overflow. So instead it's - /// rearranged to ((n - 1) / 32) + 1. - /// Due to sign extension, we don't need to special case for n == 0, if we use - /// bitwise operations (since ((n - 1) >> 5) + 1 = 0). - /// This doesn't hold true for ((n - 1) / 32) + 1, which equals 1. - /// - /// Usage: - /// GetArrayLength(77): returns how many ints must be - /// allocated to store 77 bits. - /// - /// - /// how many ints are required to store n bytes - private static int GetInt32ArrayLengthFromBitLength(int n) + /// Determines the number of s required to store bits. + private static int GetInt32ArrayLengthFromBitLength(int bitLength) { - Debug.Assert(n >= 0); - return (int)((uint)(n - 1 + (1 << BitShiftPerInt32)) >> BitShiftPerInt32); + Debug.Assert(bitLength >= 0); + return (int)(((uint)bitLength + 31u) >> 5); } - private static int GetInt32ArrayLengthFromByteLength(int n) + /// Determines the number of s required to store bits. + internal static int GetByteArrayLengthFromBitLength(int bitLength) { - // Due to sign extension, we don't need to special case for n == 0, since ((n - 1) >> 2) + 1 = 0 - // This doesn't hold true for ((n - 1) / 4) + 1, which equals 1. - Debug.Assert(n >= 0); - return (int)((uint)(n - 1 + (1 << BitShiftForBytesPerInt32)) >> BitShiftForBytesPerInt32); + Debug.Assert(bitLength >= 0); + return (int)(((uint)bitLength + 7u) >> 3); } - internal static int GetByteArrayLengthFromBitLength(int n) + /// Rounds up to a multiple of sizeof(int). + private static int RoundUpToMultipleSizeOfInt32(int value) => + (value + (sizeof(int) - 1)) & ~(sizeof(int) - 1); + + private static int GetAlignedByteArrayLength(int bitLength) => + // Always allocate in groups of sizeof(int) bytes so that we can use MemoryMarshal.Cast + // to manipulate as ints when desired. + RoundUpToMultipleSizeOfInt32(GetByteArrayLengthFromBitLength(bitLength)); + + /// Allocates a new byte array of the specified bit length, rounded up to the nearest multiple of sizeof(int). + private static byte[] AllocateByteArray(int bitLength) { - // Due to sign extension, we don't need to special case for n == 0, since ((n - 1) >> 3) + 1 = 0 - // This doesn't hold true for ((n - 1) / 8) + 1, which equals 1. - Debug.Assert(n >= 0); - return (int)((uint)(n - 1 + (1 << BitShiftPerByte)) >> BitShiftPerByte); + int byteLength = GetAlignedByteArrayLength(bitLength); + Debug.Assert(byteLength > 0, "byteLength should be positive."); + Debug.Assert(byteLength % sizeof(int) == 0, "byteLength should be a multiple of sizeof(int)."); + return new byte[byteLength]; } + /// Nop on little endian, reverses the endianness of on big endian. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int ReverseIfBE(int value) => BitConverter.IsLittleEndian ? value : BinaryPrimitives.ReverseEndianness(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static uint ReverseIfBE(uint value) => - BitConverter.IsLittleEndian ? value : BinaryPrimitives.ReverseEndianness(value); - private static void ThrowArgumentOutOfRangeException(int index) => throw new ArgumentOutOfRangeException(nameof(index), index, SR.ArgumentOutOfRange_IndexMustBeLess); private sealed class BitArrayEnumeratorSimple : IEnumerator, ICloneable { + private static readonly object s_boxedTrue = true; + private static readonly object s_boxedFalse = false; + private readonly BitArray _bitArray; private readonly int _version; private int _index; - private bool _currentElement; + private object _currentElement = s_boxedFalse; internal BitArrayEnumeratorSimple(BitArray bitArray) { @@ -949,14 +921,14 @@ public bool MoveNext() throw new InvalidOperationException(SR.InvalidOperation_EnumFailedVersion); } - if (_index < (_bitArray.m_length - 1)) + if (_index < (_bitArray._bitLength - 1)) { _index++; - _currentElement = _bitArray.Get(_index); + _currentElement = _bitArray.Get(_index) ? s_boxedTrue : s_boxedFalse; return true; } - _index = _bitArray.m_length; + _index = _bitArray._bitLength; return false; } @@ -964,7 +936,7 @@ public object Current { get { - if ((uint)_index >= (uint)_bitArray.m_length) + if ((uint)_index >= (uint)_bitArray._bitLength) { throw GetInvalidOperationException(_index); } @@ -991,7 +963,7 @@ private InvalidOperationException GetInvalidOperationException(int index) } else { - Debug.Assert(index >= _bitArray.m_length); + Debug.Assert(index >= _bitArray._bitLength); return new InvalidOperationException(SR.InvalidOperation_EnumEnded); } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/CollectionsMarshal.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/CollectionsMarshal.cs index d3286cd1e3fdcb..e52270a905fc43 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/CollectionsMarshal.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/CollectionsMarshal.cs @@ -62,9 +62,7 @@ public static Span AsSpan(List? list) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Span AsBytes(BitArray? array) => array is null ? default : - MemoryMarshal - .AsBytes((Span)array.m_array) - [..BitArray.GetByteArrayLengthFromBitLength(array.Length)]; + array._array.AsSpan(0, BitArray.GetByteArrayLengthFromBitLength(array.Length)); /// /// Gets either a ref to a in the or a ref null if it does not exist in the . From 4bd8d2276f008deed17fb63dc1c7bfa37984d587 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Tue, 10 Jun 2025 12:53:33 -0400 Subject: [PATCH 4/5] Address PR feedback --- .../tests/BitArray/BitArray_GetSetTests.cs | 32 ++++ .../src/CompatibilitySuppressions.xml | 6 + .../src/System/Collections/BitArray.cs | 152 +++++++++++++----- 3 files changed, 154 insertions(+), 36 deletions(-) diff --git a/src/libraries/System.Collections/tests/BitArray/BitArray_GetSetTests.cs b/src/libraries/System.Collections/tests/BitArray/BitArray_GetSetTests.cs index 9ea0b3c13cd01c..6a985ff182a70b 100644 --- a/src/libraries/System.Collections/tests/BitArray/BitArray_GetSetTests.cs +++ b/src/libraries/System.Collections/tests/BitArray/BitArray_GetSetTests.cs @@ -383,6 +383,38 @@ public static void AsBytes_ChangesReflectedInBitArray() Assert.True(ba[7]); } + [Fact] + public static void AsBytes_ExtraBitsDontAffectReadOperations() + { + BitArray ba = new(4); + Span bytes = CollectionsMarshal.AsBytes(ba); + Assert.Equal(0, bytes[0]); + + bytes[0] = 0xFF; + + Assert.True(ba[0]); + Assert.True(ba[1]); + Assert.True(ba[2]); + Assert.True(ba[3]); + + Assert.True(ba.HasAllSet()); + Assert.True(ba.HasAnySet()); + + ba[0] = false; + + Assert.False(ba.HasAllSet()); + Assert.True(ba.HasAnySet()); + + ba[1] = false; + ba[2] = false; + ba[3] = false; + + Assert.False(ba.HasAllSet()); + Assert.False(ba.HasAnySet()); + + Assert.Equal(0b11110000, bytes[0]); + } + [Fact] public static void CopyToByteArray() { diff --git a/src/libraries/System.Private.CoreLib/src/CompatibilitySuppressions.xml b/src/libraries/System.Private.CoreLib/src/CompatibilitySuppressions.xml index 82263c77811d86..21e9323f9e7168 100644 --- a/src/libraries/System.Private.CoreLib/src/CompatibilitySuppressions.xml +++ b/src/libraries/System.Private.CoreLib/src/CompatibilitySuppressions.xml @@ -25,4 +25,10 @@ CP0002 M:System.String.TrimStart(System.ReadOnlySpan{System.Char}) + + CP0008 + T:System.Collections.BitArray + ref/net10.0/System.Private.CoreLib.dll + lib/net10.0/System.Private.CoreLib.dll + diff --git a/src/libraries/System.Private.CoreLib/src/System/Collections/BitArray.cs b/src/libraries/System.Private.CoreLib/src/System/Collections/BitArray.cs index dc18b30f9392d0..a5889cd2682a87 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Collections/BitArray.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Collections/BitArray.cs @@ -9,6 +9,7 @@ using System.Runtime.Intrinsics; using System.Runtime.Intrinsics.Arm; using System.Runtime.Intrinsics.X86; +using System.Runtime.Serialization; namespace System.Collections { @@ -17,8 +18,10 @@ namespace System.Collections /// indicates that the bit is on (1) and indicates /// the bit is off (0). /// - [Serializable] // leaving it serializable, but no longer compatible with pre-.NET 10 serialization format - public sealed class BitArray : ICollection, ICloneable + [Serializable] + [TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")] + public sealed class BitArray : ICollection, ICloneable, + ISerializable // introduced in .NET 10 for compat with existing serialized assets, not exposed in the ref assembly { /// sizeof(int) * 8 private const int BitsPerInt32 = 32; @@ -71,6 +74,44 @@ public BitArray(int length, bool defaultValue) } } + /// Deserializes BitArray in a way that's compatible with the original .NET Framework implementation. + private BitArray(SerializationInfo info, StreamingContext context) + { + ArgumentNullException.ThrowIfNull(info); + + var array = (int[]?)info.GetValue("m_array", typeof(int[])); + _bitLength = info.GetInt32("m_length"); + _version = info.GetInt32("_version"); + + if (array is null || (uint)_bitLength > checked((uint)array.Length * BitsPerInt32)) + { + throw new SerializationException(SR.Serialization_InvalidData); + } + + _array = AllocateByteArray(_bitLength); + if (BitConverter.IsLittleEndian) + { + MemoryMarshal.AsBytes(array).CopyTo(_array); + } + else + { + BinaryPrimitives.ReverseEndianness(array, MemoryMarshal.Cast((Span)_array)); + } + } + + /// Generates serialization data for the BitArray in a way that's compatible with the original .NET Framework implementation. + void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context) + { + ArgumentNullException.ThrowIfNull(info); + + var m_array = new int[GetInt32ArrayLengthFromBitLength(_bitLength)]; + CopyTo(m_array, 0); + + info.AddValue("m_array", m_array); + info.AddValue("m_length", _bitLength); + info.AddValue("_version", _version); + } + private void ClearHighExtraBits() { (uint index, uint extraBits) = Math.DivRem((uint)_bitLength, BitsPerInt32); @@ -204,7 +245,7 @@ public BitArray(int[] values) if (BitConverter.IsLittleEndian) { - MemoryMarshal.AsBytes(values.AsSpan()).CopyTo(_array); + MemoryMarshal.AsBytes(values).CopyTo(_array); } else { @@ -304,7 +345,7 @@ public void SetAll(bool value) } else { - MemoryMarshal.Cast((Span)_array).Slice(0, GetInt32ArrayLengthFromBitLength(_bitLength)).Clear(); + _array.AsSpan(0, GetByteArrayLengthFromBitLength(_bitLength)).Clear(); } _version++; @@ -472,14 +513,14 @@ public BitArray RightShift(int count) return this; } - Span array = MemoryMarshal.Cast((Span)_array); + Span intSpan = MemoryMarshal.Cast((Span)_array); int toIndex = 0; int ints = GetInt32ArrayLengthFromBitLength(_bitLength); if (count < _bitLength) { // We can not use Math.DivRem without taking a dependency on System.Runtime.Extensions - (int fromIndex, int shiftCount) = Math.DivRem((int)count, 32); + (int fromIndex, int shiftCount) = Math.DivRem(count, 32); int extraBits = (int)((uint)_bitLength % 32); if (shiftCount == 0) { @@ -492,10 +533,10 @@ public BitArray RightShift(int count) // 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); - array[ints - 1] &= (int)mask; + intSpan[ints - 1] &= ReverseIfBE((int)mask); - array.Slice((int)fromIndex, ints - fromIndex).CopyTo(array); - toIndex = ints - (int)fromIndex; + intSpan.Slice((int)fromIndex, ints - fromIndex).CopyTo(intSpan); + toIndex = ints - fromIndex; } else { @@ -503,18 +544,18 @@ public BitArray RightShift(int count) while (fromIndex < lastIndex) { - uint right = (uint)array[fromIndex] >> shiftCount; - int left = array[++fromIndex] << (BitsPerInt32 - shiftCount); - array[toIndex++] = left | (int)right; + uint right = (uint)ReverseIfBE(intSpan[fromIndex]) >> shiftCount; + int left = ReverseIfBE(intSpan[++fromIndex]) << (BitsPerInt32 - shiftCount); + intSpan[toIndex++] = ReverseIfBE(left | (int)right); } uint mask = uint.MaxValue >> (BitsPerInt32 - extraBits); - mask &= (uint)array[fromIndex]; - array[toIndex++] = (int)(mask >> shiftCount); + mask &= (uint)ReverseIfBE(intSpan[fromIndex]); + intSpan[toIndex++] = ReverseIfBE((int)(mask >> shiftCount)); } } - array.Slice(toIndex, ints - toIndex).Clear(); + intSpan.Slice(toIndex, ints - toIndex).Clear(); _version++; return this; } @@ -535,7 +576,7 @@ public BitArray LeftShift(int count) return this; } - Span span = MemoryMarshal.Cast((Span)_array); + Span intSpan = MemoryMarshal.Cast((Span)_array); int lengthToClear; if (count < _bitLength) @@ -546,7 +587,7 @@ public BitArray LeftShift(int count) if (shiftCount == 0) { - span.Slice(0, lastIndex + 1 - lengthToClear).CopyTo(span.Slice(lengthToClear)); + intSpan.Slice(0, lastIndex + 1 - lengthToClear).CopyTo(intSpan.Slice(lengthToClear)); } else { @@ -554,12 +595,12 @@ public BitArray LeftShift(int count) while (fromindex > 0) { - int left = ReverseIfBE(span[fromindex]) << shiftCount; - uint right = (uint)ReverseIfBE(span[--fromindex]) >> (BitsPerInt32 - shiftCount); - span[lastIndex] = ReverseIfBE(left | (int)right); + int left = ReverseIfBE(intSpan[fromindex]) << shiftCount; + uint right = (uint)ReverseIfBE(intSpan[--fromindex]) >> (BitsPerInt32 - shiftCount); + intSpan[lastIndex] = ReverseIfBE(left | (int)right); lastIndex--; } - span[lastIndex] = ReverseIfBE(ReverseIfBE(span[fromindex]) << shiftCount); + intSpan[lastIndex] = ReverseIfBE(ReverseIfBE(intSpan[fromindex]) << shiftCount); } } else @@ -567,7 +608,7 @@ public BitArray LeftShift(int count) lengthToClear = GetInt32ArrayLengthFromBitLength(_bitLength); // Clear all } - span.Slice(0, lengthToClear).Clear(); + intSpan.Slice(0, lengthToClear).Clear(); _version++; return this; } @@ -627,21 +668,30 @@ public unsafe void CopyTo(Array array, int index) if (array is int[] intArray) { - int int32Length = GetInt32ArrayLengthFromBitLength(_bitLength); + int intLength = GetInt32ArrayLengthFromBitLength(_bitLength); - if (array.Length - index < int32Length) + if (array.Length - index < intLength) { throw new ArgumentException(SR.Argument_InvalidOffLen); } - Span source = MemoryMarshal.Cast((Span)_array).Slice(0, int32Length); - if (BitConverter.IsLittleEndian) + if (intLength > 0) { - source.CopyTo(intArray.AsSpan(index)); - } - else - { - BinaryPrimitives.ReverseEndianness(source, intArray.AsSpan(index)); + Span source = MemoryMarshal.Cast((Span)_array).Slice(0, intLength); + if (BitConverter.IsLittleEndian) + { + source.CopyTo(intArray.AsSpan(index)); + } + else + { + BinaryPrimitives.ReverseEndianness(source, intArray.AsSpan(index)); + } + + uint extraBits = (uint)_bitLength % BitsPerInt32; + if (extraBits != 0) + { + intArray[index + intLength - 1] = ReverseIfBE(source[^1]) & ((1 << (int)extraBits) - 1); + } } } else if (array is byte[] byteArray) @@ -653,7 +703,17 @@ public unsafe void CopyTo(Array array, int index) throw new ArgumentException(SR.Argument_InvalidOffLen); } - _array.AsSpan(0, byteLength).CopyTo(byteArray.AsSpan(index)); + if (byteLength > 0) + { + ReadOnlySpan source = _array.AsSpan(0, byteLength); + source.CopyTo(byteArray.AsSpan(index)); + + uint extraBits = (uint)_bitLength % BitsPerByte; + if (extraBits != 0) + { + byteArray[index + byteLength - 1] = (byte)(source[^1] & ((1 << (int)extraBits) - 1)); + } + } } else if (array is bool[] boolArray) { @@ -808,7 +868,7 @@ public unsafe void CopyTo(Array array, int index) /// true if every bit in the is set to true, or if is empty; otherwise, false. public bool HasAllSet() { - int extraBits = (int)((uint)_bitLength % BitsPerByte); + uint extraBits = (uint)_bitLength % BitsPerByte; int byteCount = GetByteArrayLengthFromBitLength(_bitLength); if (extraBits != 0) { @@ -825,7 +885,7 @@ public bool HasAllSet() return true; } - byte mask = (byte)((1 << extraBits) - 1); + byte mask = (byte)((1 << (int)extraBits) - 1); return (_array[byteCount] & mask) == mask; } @@ -833,8 +893,28 @@ public bool HasAllSet() /// Determines whether any bit in the is set to true. /// /// true if is not empty and at least one of its bit is set to true; otherwise, false. - public bool HasAnySet() => - _array.AsSpan(0, GetByteArrayLengthFromBitLength(_bitLength)).ContainsAnyExcept((byte)0); + public bool HasAnySet() + { + uint extraBits = (uint)_bitLength % BitsPerByte; + int byteCount = GetByteArrayLengthFromBitLength(_bitLength); + if (extraBits != 0) + { + byteCount--; + } + + if (_array.AsSpan(0, byteCount).ContainsAnyExcept((byte)0)) + { + return true; + } + + if (extraBits == 0) + { + return false; + } + + byte mask = (byte)((1 << (int)extraBits) - 1); + return (_array[byteCount] & mask) != 0; + } /// Gets the number of elements contained in the . public int Count => _bitLength; From 8c3963ce3cc23dba349556b1e618d720ab700f80 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Wed, 11 Jun 2025 08:40:11 -0400 Subject: [PATCH 5/5] Fix assert --- .../System.Private.CoreLib/src/System/Collections/BitArray.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Collections/BitArray.cs b/src/libraries/System.Private.CoreLib/src/System/Collections/BitArray.cs index a5889cd2682a87..66fe6f6c37eff6 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Collections/BitArray.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Collections/BitArray.cs @@ -962,9 +962,9 @@ private static int GetAlignedByteArrayLength(int bitLength) => private static byte[] AllocateByteArray(int bitLength) { int byteLength = GetAlignedByteArrayLength(bitLength); - Debug.Assert(byteLength > 0, "byteLength should be positive."); + Debug.Assert(byteLength >= 0, "byteLength should be non-negative."); Debug.Assert(byteLength % sizeof(int) == 0, "byteLength should be a multiple of sizeof(int)."); - return new byte[byteLength]; + return bitLength != 0 ? new byte[byteLength] : []; } /// Nop on little endian, reverses the endianness of on big endian.