test: bring coverage to 100% for integer Unpack (#70)

This commit is contained in:
Oliver Booth 2023-04-01 21:56:45 +01:00
parent 5f21a2102d
commit 87b6dbdd56
No known key found for this signature in database
GPG Key ID: 20BEB9DC87961025
13 changed files with 390 additions and 149 deletions

View File

@ -1,4 +1,5 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using X10D.Collections;
namespace X10D.Tests.Collections;
@ -7,7 +8,7 @@ namespace X10D.Tests.Collections;
public class ByteTests
{
[TestMethod]
public void UnpackBits_ShouldUnpackToArrayCorrectly()
public void Unpack_ShouldUnpackToArrayCorrectly()
{
bool[] bits = ((byte)0b11010100).Unpack();
@ -24,7 +25,7 @@ public class ByteTests
}
[TestMethod]
public void UnpackBits_ShouldUnpackToSpanCorrectly()
public void Unpack_ShouldUnpackToSpanCorrectly()
{
Span<bool> bits = stackalloc bool[8];
((byte)0b11010100).Unpack(bits);
@ -39,14 +40,35 @@ public class ByteTests
Assert.IsTrue(bits[7]);
}
#if NET5_0_OR_GREATER
[TestMethod]
public void UnpackBits_ShouldRepackEqually()
public void Unpack_ShouldUnpackToSpanCorrectly_GivenFallbackImplementation()
{
var mock = new Mock<ISsse3SupportProvider>();
mock.Setup(provider => provider.IsSupported).Returns(false);
Span<bool> bits = stackalloc bool[8];
((byte)0b11010100).UnpackInternal(bits, mock.Object);
Assert.IsFalse(bits[0]);
Assert.IsFalse(bits[1]);
Assert.IsTrue(bits[2]);
Assert.IsFalse(bits[3]);
Assert.IsTrue(bits[4]);
Assert.IsFalse(bits[5]);
Assert.IsTrue(bits[6]);
Assert.IsTrue(bits[7]);
}
#endif
[TestMethod]
public void Unpack_ShouldRepackEqually()
{
Assert.AreEqual(0b11010100, ((byte)0b11010100).Unpack().PackByte());
}
[TestMethod]
public void UnpackBits_ShouldThrow_GivenTooSmallSpan()
public void Unpack_ShouldThrow_GivenTooSmallSpan()
{
Assert.ThrowsException<ArgumentException>(() =>
{

View File

@ -1,4 +1,5 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using X10D.Collections;
namespace X10D.Tests.Collections;
@ -7,7 +8,7 @@ namespace X10D.Tests.Collections;
public class Int16Tests
{
[TestMethod]
public void UnpackBits_ShouldUnpackToArrayCorrectly()
public void Unpack_ShouldUnpackToArrayCorrectly()
{
bool[] bits = ((short)0b11010100).Unpack();
@ -29,7 +30,7 @@ public class Int16Tests
}
[TestMethod]
public void UnpackBits_ShouldUnpackToSpanCorrectly()
public void Unpack_ShouldUnpackToSpanCorrectly()
{
Span<bool> bits = stackalloc bool[16];
((short)0b11010100).Unpack(bits);
@ -49,14 +50,40 @@ public class Int16Tests
}
}
#if NET5_0_OR_GREATER
[TestMethod]
public void UnpackBits_ShouldRepackEqually()
public void Unpack_ShouldUnpackToSpanCorrectly_GivenFallbackImplementation()
{
var mock = new Mock<ISsse3SupportProvider>();
mock.Setup(provider => provider.IsSupported).Returns(false);
Span<bool> bits = stackalloc bool[16];
((short)0b11010100).UnpackInternal(bits, mock.Object);
Assert.IsFalse(bits[0]);
Assert.IsFalse(bits[1]);
Assert.IsTrue(bits[2]);
Assert.IsFalse(bits[3]);
Assert.IsTrue(bits[4]);
Assert.IsFalse(bits[5]);
Assert.IsTrue(bits[6]);
Assert.IsTrue(bits[7]);
for (var index = 8; index < 16; index++)
{
Assert.IsFalse(bits[index]);
}
}
#endif
[TestMethod]
public void Unpack_ShouldRepackEqually()
{
Assert.AreEqual(0b11010100, ((short)0b11010100).Unpack().PackInt16());
}
[TestMethod]
public void UnpackBits_ShouldThrow_GivenTooSmallSpan()
public void Unpack_ShouldThrow_GivenTooSmallSpan()
{
Assert.ThrowsException<ArgumentException>(() =>
{

View File

@ -1,4 +1,5 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using X10D.Collections;
namespace X10D.Tests.Collections;
@ -7,7 +8,7 @@ namespace X10D.Tests.Collections;
public class Int32Tests
{
[TestMethod]
public void UnpackBits_ShouldUnpackToArrayCorrectly()
public void Unpack_ShouldUnpackToArrayCorrectly()
{
bool[] bits = 0b11010100.Unpack();
@ -29,7 +30,7 @@ public class Int32Tests
}
[TestMethod]
public void UnpackBits_ShouldUnpackToSpanCorrectly()
public void Unpack_ShouldUnpackToSpanCorrectly()
{
Span<bool> bits = stackalloc bool[32];
0b11010100.Unpack(bits);
@ -49,14 +50,69 @@ public class Int32Tests
}
}
#if NET5_0_OR_GREATER
[TestMethod]
public void UnpackBits_ShouldRepackEqually()
public void Unpack_ShouldUnpackToSpanCorrectly_GivenFallbackFromAvx2()
{
var ssse3Mock = new Mock<ISsse3SupportProvider>();
var avx2Mock = new Mock<IAvx2SupportProvider>();
avx2Mock.Setup(provider => provider.IsSupported).Returns(false);
ssse3Mock.Setup(provider => provider.IsSupported).Returns(true);
Span<bool> bits = stackalloc bool[32];
0b11010100.UnpackInternal(bits, ssse3Mock.Object, avx2Mock.Object);
Assert.IsFalse(bits[0]);
Assert.IsFalse(bits[1]);
Assert.IsTrue(bits[2]);
Assert.IsFalse(bits[3]);
Assert.IsTrue(bits[4]);
Assert.IsFalse(bits[5]);
Assert.IsTrue(bits[6]);
Assert.IsTrue(bits[7]);
for (var index = 8; index < 32; index++)
{
Assert.IsFalse(bits[index]);
}
}
[TestMethod]
public void Unpack_ShouldUnpackToSpanCorrectly_GivenFallback()
{
var ssse3Mock = new Mock<ISsse3SupportProvider>();
var avx2Mock = new Mock<IAvx2SupportProvider>();
ssse3Mock.Setup(provider => provider.IsSupported).Returns(false);
avx2Mock.Setup(provider => provider.IsSupported).Returns(false);
Span<bool> bits = stackalloc bool[32];
0b11010100.UnpackInternal(bits, ssse3Mock.Object, avx2Mock.Object);
Assert.IsFalse(bits[0]);
Assert.IsFalse(bits[1]);
Assert.IsTrue(bits[2]);
Assert.IsFalse(bits[3]);
Assert.IsTrue(bits[4]);
Assert.IsFalse(bits[5]);
Assert.IsTrue(bits[6]);
Assert.IsTrue(bits[7]);
for (var index = 8; index < 32; index++)
{
Assert.IsFalse(bits[index]);
}
}
#endif
[TestMethod]
public void Unpack_ShouldRepackEqually()
{
Assert.AreEqual(0b11010100, 0b11010100.Unpack().PackInt32());
}
[TestMethod]
public void UnpackBits_ShouldThrow_GivenTooSmallSpan()
public void Unpack_ShouldThrow_GivenTooSmallSpan()
{
Assert.ThrowsException<ArgumentException>(() =>
{

View File

@ -1,4 +1,5 @@
using System.Diagnostics.Contracts;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
#if NETCOREAPP3_0_OR_GREATER
using System.Runtime.Intrinsics;
@ -33,25 +34,51 @@ public static class ByteExtensions
/// <param name="value">The value to unpack.</param>
/// <param name="destination">When this method returns, contains the unpacked booleans from <paramref name="value" />.</param>
/// <exception cref="ArgumentException"><paramref name="destination" /> is not large enough to contain the result.</exception>
[ExcludeFromCodeCoverage]
public static void Unpack(this byte value, Span<bool> destination)
{
if (destination.Length < Size)
{
throw new ArgumentException($"Destination must be at least {Size} in length.", nameof(destination));
#if NETCOREAPP3_0_OR_GREATER
UnpackInternal(value, destination, new SystemSsse3SupportProvider());
#else
UnpackInternal(value, destination);
#endif
}
#if NETCOREAPP3_0_OR_GREATER
if (Ssse3.IsSupported)
internal static void UnpackInternal(this byte value, Span<bool> destination, ISsse3SupportProvider? ssse3SupportProvider)
#else
internal static void UnpackInternal(this byte value, Span<bool> destination)
#endif
{
Ssse3Implementation(value, destination);
if (destination.Length < Size)
{
throw new ArgumentException(ExceptionMessages.DestinationSpanLengthTooShort, nameof(destination));
}
#if NETCOREAPP3_0_OR_GREATER
ssse3SupportProvider ??= new SystemSsse3SupportProvider();
if (ssse3SupportProvider.IsSupported)
{
UnpackInternal_Ssse3(value, destination);
return;
}
#endif
FallbackImplementation(value, destination);
UnpackInternal_Fallback(value, destination);
}
private static void UnpackInternal_Fallback(byte value, Span<bool> destination)
{
for (var index = 0; index < Size; index++)
{
destination[index] = (value & (1 << index)) != 0;
}
}
#if NETCOREAPP3_0_OR_GREATER
unsafe static void Ssse3Implementation(byte value, Span<bool> destination)
private unsafe static void UnpackInternal_Ssse3(byte value, Span<bool> destination)
{
fixed (bool* pDestination = destination)
{
@ -71,12 +98,4 @@ public static class ByteExtensions
}
}
#endif
static void FallbackImplementation(byte value, Span<bool> destination)
{
for (var index = 0; index < Size; index++)
{
destination[index] = (value & (1 << index)) != 0;
}
}
}
}

View File

@ -35,23 +35,56 @@ public static class Int16Extensions
/// <exception cref="ArgumentException"><paramref name="destination" /> is not large enough to contain the result.</exception>
public static void Unpack(this short value, Span<bool> destination)
{
if (destination.Length < Size)
{
throw new ArgumentException($"Destination must be at least {Size} in length.", nameof(destination));
#if NETCOREAPP3_0_OR_GREATER
UnpackInternal(value, destination, new SystemSsse3SupportProvider());
#else
UnpackInternal(value, destination);
#endif
}
#if NETCOREAPP3_0_OR_GREATER
if (Ssse3.IsSupported)
internal static void UnpackInternal(this short value, Span<bool> destination, ISsse3SupportProvider? ssse3SupportProvider)
#else
internal static void UnpackInternal(this short value, Span<bool> destination)
#endif
{
Ssse3Implementation(value, destination);
if (destination.Length < Size)
{
throw new ArgumentException(ExceptionMessages.DestinationSpanLengthTooShort, nameof(destination));
}
#if NETCOREAPP3_0_OR_GREATER
ssse3SupportProvider ??= new SystemSsse3SupportProvider();
if (ssse3SupportProvider.IsSupported)
{
UnpackInternal_Ssse3(value, destination);
return;
}
#endif
FallbackImplementation(value, destination);
UnpackInternal_Fallback(value, destination);
}
private static void UnpackInternal_Fallback(short value, Span<bool> destination)
{
for (var index = 0; index < Size; index++)
{
destination[index] = (value & (1 << index)) != 0;
}
}
#if NETCOREAPP3_0_OR_GREATER
unsafe static void Ssse3Implementation(short value, Span<bool> destination)
private struct SystemSsse3SupportProvider : ISsse3SupportProvider
{
/// <inheritdoc />
public bool IsSupported
{
get => Sse3.IsSupported;
}
}
private unsafe static void UnpackInternal_Ssse3(short value, Span<bool> destination)
{
fixed (bool* pDestination = destination)
{
@ -59,26 +92,18 @@ public static class Int16Extensions
0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80,
0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80
);
var mask1Lo = Vector128.Create((byte)0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1);
var mask1Lo = Vector128.Create((byte)0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1);
var one = Vector128.Create((byte)0x01);
var vec = Vector128.Create(value).AsByte();
var shuffle = Ssse3.Shuffle(vec, mask1Lo);
var and = Sse2.AndNot(shuffle, mask2);
var cmp = Sse2.CompareEqual(and, Vector128<byte>.Zero);
var correctness = Sse2.And(cmp, one);
Vector128<byte> vec = Vector128.Create(value).AsByte();
Vector128<byte> shuffle = Ssse3.Shuffle(vec, mask1Lo);
Vector128<byte> and = Sse2.AndNot(shuffle, mask2);
Vector128<byte> cmp = Sse2.CompareEqual(and, Vector128<byte>.Zero);
Vector128<byte> correctness = Sse2.And(cmp, one);
Sse2.Store((byte*)pDestination, correctness);
}
}
#endif
static void FallbackImplementation(short value, Span<bool> destination)
{
for (var index = 0; index < Size; index++)
{
destination[index] = (value & (1 << index)) != 0;
}
}
}
}

View File

@ -1,4 +1,5 @@
using System.Diagnostics.Contracts;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
#if NETCOREAPP3_0_OR_GREATER
using System.Runtime.Intrinsics;
@ -33,34 +34,90 @@ public static class Int32Extensions
/// <param name="value">The value to unpack.</param>
/// <param name="destination">When this method returns, contains the unpacked booleans from <paramref name="value" />.</param>
/// <exception cref="ArgumentException"><paramref name="destination" /> is not large enough to contain the result.</exception>
[ExcludeFromCodeCoverage]
public static void Unpack(this int value, Span<bool> destination)
{
#if NETCOREAPP3_0_OR_GREATER
UnpackInternal(value, destination, new SystemSsse3SupportProvider(), new SystemAvx2SupportProvider());
#else
UnpackInternal(value, destination);
#endif
}
internal static void UnpackInternal(this int value,
Span<bool> destination
#if NETCOREAPP3_0_OR_GREATER
,
ISsse3SupportProvider? ssse3SupportProvider,
IAvx2SupportProvider? avx2SupportProvider
#endif
)
{
if (destination.Length < Size)
{
throw new ArgumentException($"Destination must be at least {Size} in length.", nameof(destination));
throw new ArgumentException(ExceptionMessages.DestinationSpanLengthTooShort, nameof(destination));
}
#if NETCOREAPP3_0_OR_GREATER
// TODO: AdvSimd support.
ssse3SupportProvider ??= new SystemSsse3SupportProvider();
avx2SupportProvider ??= new SystemAvx2SupportProvider();
// https://stackoverflow.com/questions/24225786/fastest-way-to-unpack-32-bits-to-a-32-byte-simd-vector
if (Avx2.IsSupported)
if (avx2SupportProvider.IsSupported)
{
Avx2Implementation(value, destination);
UnpackInternal_Avx2(value, destination);
return;
}
if (Ssse3.IsSupported)
if (ssse3SupportProvider.IsSupported)
{
Ssse3Implementation(value, destination);
UnpackInternal_Ssse3(value, destination);
return;
}
#endif
FallbackImplementation(value, destination);
UnpackInternal_Fallback(value, destination);
}
private static void UnpackInternal_Fallback(int value, Span<bool> destination)
{
for (var index = 0; index < Size; index++)
{
destination[index] = (value & (1 << index)) != 0;
}
}
#if NETCOREAPP3_0_OR_GREATER
unsafe static void Avx2Implementation(int value, Span<bool> destination)
private static unsafe void UnpackInternal_Ssse3(int value, Span<bool> destination)
{
fixed (bool* pDestination = destination)
{
var mask2 = Vector128.Create(
0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80,
0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80
);
var mask1Lo = Vector128.Create((byte)0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1);
var mask1Hi = Vector128.Create((byte)2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3);
var one = Vector128.Create((byte)0x01);
Vector128<byte> vec = Vector128.Create(value).AsByte();
Vector128<byte> shuffle = Ssse3.Shuffle(vec, mask1Lo);
Vector128<byte> and = Sse2.AndNot(shuffle, mask2);
Vector128<byte> cmp = Sse2.CompareEqual(and, Vector128<byte>.Zero);
Vector128<byte> correctness = Sse2.And(cmp, one);
Sse2.Store((byte*)pDestination, correctness);
shuffle = Ssse3.Shuffle(vec, mask1Hi);
and = Sse2.AndNot(shuffle, mask2);
cmp = Sse2.CompareEqual(and, Vector128<byte>.Zero);
correctness = Sse2.And(cmp, one);
Sse2.Store((byte*)pDestination + 16, correctness);
}
}
private static unsafe void UnpackInternal_Avx2(int value, Span<bool> destination)
{
fixed (bool* pDestination = destination)
{
@ -77,52 +134,14 @@ public static class Int32Extensions
0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80
);
var vec = Vector256.Create(value).AsByte();
var shuffle = Avx2.Shuffle(vec, mask1);
var and = Avx2.AndNot(shuffle, mask2);
var cmp = Avx2.CompareEqual(and, Vector256<byte>.Zero);
var correctness = Avx2.And(cmp, Vector256.Create((byte)0x01));
Vector256<byte> vec = Vector256.Create(value).AsByte();
Vector256<byte> shuffle = Avx2.Shuffle(vec, mask1);
Vector256<byte> and = Avx2.AndNot(shuffle, mask2);
Vector256<byte> cmp = Avx2.CompareEqual(and, Vector256<byte>.Zero);
Vector256<byte> correctness = Avx2.And(cmp, Vector256.Create((byte)0x01));
Avx.Store((byte*)pDestination, correctness);
}
}
unsafe static void Ssse3Implementation(int value, Span<bool> destination)
{
fixed (bool* pDestination = destination)
{
var mask2 = Vector128.Create(
0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80,
0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80
);
var mask1Lo = Vector128.Create((byte)0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1);
var mask1Hi = Vector128.Create((byte)2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3);
var one = Vector128.Create((byte)0x01);
var vec = Vector128.Create(value).AsByte();
var shuffle = Ssse3.Shuffle(vec, mask1Lo);
var and = Sse2.AndNot(shuffle, mask2);
var cmp = Sse2.CompareEqual(and, Vector128<byte>.Zero);
var correctness = Sse2.And(cmp, one);
Sse2.Store((byte*)pDestination, correctness);
shuffle = Ssse3.Shuffle(vec, mask1Hi);
and = Sse2.AndNot(shuffle, mask2);
cmp = Sse2.CompareEqual(and, Vector128<byte>.Zero);
correctness = Sse2.And(cmp, one);
Sse2.Store((byte*)pDestination + 16, correctness);
}
}
#endif
static void FallbackImplementation(int value, Span<bool> destination)
{
for (var index = 0; index < Size; index++)
{
destination[index] = (value & (1 << index)) != 0;
}
}
}
}

View File

@ -32,7 +32,7 @@ public static class Int64Extensions
{
if (destination.Length < Size)
{
throw new ArgumentException($"Destination must be at least {Size} in length.", nameof(destination));
throw new ArgumentException(ExceptionMessages.DestinationSpanLengthTooShort, nameof(destination));
}
for (var index = 0; index < Size; index++)

View File

@ -1,7 +1,6 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
@ -87,6 +86,15 @@ namespace X10D {
}
}
/// <summary>
/// Looks up a localized string similar to The destination span is too short to contain the data..
/// </summary>
internal static string DestinationSpanLengthTooShort {
get {
return ResourceManager.GetString("DestinationSpanLengthTooShort", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The end index must be less than the list count..
/// </summary>

View File

@ -123,6 +123,9 @@
<data name="CountMustBeInRange" xml:space="preserve">
<value>Count must be positive and count must refer to a location within the string/array/collection.</value>
</data>
<data name="DestinationSpanLengthTooShort" xml:space="preserve">
<value>The destination span is too short to contain the data.</value>
</data>
<data name="EndIndexLessThanStartIndex" xml:space="preserve">
<value>The end index must be greater than or equal to the start index.</value>
</data>

View File

@ -0,0 +1,13 @@
namespace X10D;
/// <summary>
/// Represents an object which provides the status of the support of AVX2 instructions.
/// </summary>
public interface IAvx2SupportProvider
{
/// <summary>
/// Gets a value indicating whether AVX2 instructions are supported on this platform.
/// </summary>
/// <value><see langword="true" /> if AVX2 instructions are supported; otherwise, <see langword="false" />.</value>
bool IsSupported { get; }
}

View File

@ -0,0 +1,13 @@
namespace X10D;
/// <summary>
/// Represents an object which provides the status of the support of SSSE3 instructions.
/// </summary>
public interface ISsse3SupportProvider
{
/// <summary>
/// Gets a value indicating whether SSSE3 instructions are supported on this platform.
/// </summary>
/// <value><see langword="true" /> if SSSE3 instructions are supported; otherwise, <see langword="false" />.</value>
bool IsSupported { get; }
}

View File

@ -0,0 +1,18 @@
#if NETCOREAPP3_0_OR_GREATER
using System.Runtime.Intrinsics.X86;
#endif
namespace X10D;
internal struct SystemAvx2SupportProvider : IAvx2SupportProvider
{
/// <inheritdoc />
public bool IsSupported
{
#if NETCOREAPP3_0_OR_GREATER
get => Avx2.IsSupported;
#else
get => false;
#endif
}
}

View File

@ -0,0 +1,18 @@
#if NETCOREAPP3_0_OR_GREATER
using System.Runtime.Intrinsics.X86;
#endif
namespace X10D;
internal struct SystemSsse3SupportProvider : ISsse3SupportProvider
{
/// <inheritdoc />
public bool IsSupported
{
#if NETCOREAPP3_0_OR_GREATER
get => Sse3.IsSupported;
#else
get => false;
#endif
}
}