Browse Source

Fix tensor negation on ARM64

pull/3161/head
James Jackson-South 3 weeks ago
parent
commit
b54c5caf5b
  1. 52
      src/ImageSharp/Common/Helpers/TensorPrimitives_.Negate.cs
  2. 91
      tests/ImageSharp.Tests/Common/TensorPrimitivesTests.cs

52
src/ImageSharp/Common/Helpers/TensorPrimitives_.Negate.cs

@ -264,12 +264,58 @@ internal static partial class TensorPrimitives_
public static T Invoke(T x) => -x; public static T Invoke(T x) => -x;
/// <inheritdoc /> /// <inheritdoc />
public static Vector128<T> Invoke(Vector128<T> x) => -x; public static Vector128<T> Invoke(Vector128<T> x)
{
if (typeof(T) == typeof(float))
{
// IEEE-754 negation toggles the sign bit. Expressing that operation explicitly avoids the
// subtraction-based ARM64 code generated by .NET 8 for generic vector negation, which loses
// the sign when +0F is negated and therefore differs from both scalar and runtime-main behavior.
return x ^ Vector128.Create(-0F).As<float, T>();
}
if (typeof(T) == typeof(double))
{
// Double-precision values use the same sign-bit representation, with the sign in bit 63.
return x ^ Vector128.Create(-0D).As<double, T>();
}
return -x;
}
/// <inheritdoc /> /// <inheritdoc />
public static Vector256<T> Invoke(Vector256<T> x) => -x; public static Vector256<T> Invoke(Vector256<T> x)
{
if (typeof(T) == typeof(float))
{
// Keep the operation bitwise at every width so ARM64 preserves signed zero exactly.
return x ^ Vector256.Create(-0F).As<float, T>();
}
if (typeof(T) == typeof(double))
{
return x ^ Vector256.Create(-0D).As<double, T>();
}
return -x;
}
/// <inheritdoc /> /// <inheritdoc />
public static Vector512<T> Invoke(Vector512<T> x) => -x; public static Vector512<T> Invoke(Vector512<T> x)
{
if (typeof(T) == typeof(float))
{
// Vector512 can be hardware accelerated directly or decomposed by the runtime; the explicit
// bit operation provides identical IEEE-754 behavior in either case.
return x ^ Vector512.Create(-0F).As<float, T>();
}
if (typeof(T) == typeof(double))
{
return x ^ Vector512.Create(-0D).As<double, T>();
}
return -x;
}
} }
} }

91
tests/ImageSharp.Tests/Common/TensorPrimitivesTests.cs

@ -2,16 +2,14 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Tests.TestUtilities;
namespace SixLabors.ImageSharp.Tests.Common; namespace SixLabors.ImageSharp.Tests.Common;
public class TensorPrimitivesTests public class TensorPrimitivesTests
{ {
/// <summary> private static readonly int[] SpanLengthValues =
/// Gets lengths that exercise scalar execution, every SIMD width, overlapping tails, and the unrolled loop. [
/// </summary>
public static TheoryData<int> SpanLengths => new()
{
0, 0,
1, 1,
3, 3,
@ -33,7 +31,52 @@ public class TensorPrimitivesTests
128, 128,
129, 129,
2048 2048
}; ];
/// <summary>
/// Gets lengths that exercise scalar execution, every SIMD width, overlapping tails, and the unrolled loop.
/// </summary>
public static TheoryData<int> SpanLengths => new(SpanLengthValues);
/// <summary>
/// Verifies every compatibility operation while forcing the supported SIMD feature tiers in isolated processes.
/// </summary>
[Fact]
public void OperationsMatchScalarFormulasAcrossHardwareIntrinsicFeatures()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunOperationsAcrossHardwareIntrinsicFeatures,
HwIntrinsics.AllowAll
| HwIntrinsics.DisableAVX512F
| HwIntrinsics.DisableAVX
| HwIntrinsics.DisableArm64Sve
| HwIntrinsics.DisableHWIntrinsic);
/// <summary>
/// Runs the TensorPrimitives compatibility assertions inside a process configured for one hardware-intrinsic tier.
/// </summary>
private static void RunOperationsAcrossHardwareIntrinsicFeatures()
{
TensorPrimitivesTests tests = new();
// Reuse the focused assertions so the remote feature matrix cannot drift from the normal test coverage.
foreach (int length in SpanLengthValues)
{
tests.AddByteMatchesScalarFormula(length);
tests.AddUInt32MatchesScalarFormula(length);
tests.AddScalarInt32MatchesScalarFormula(length);
tests.NegateSingleMatchesScalarFormula(length);
tests.NegateDoubleMatchesScalarFormula(length);
tests.ClampInt32MatchesScalarFormula(length);
tests.ClampSingleMatchesRuntimeFormula(length);
tests.DivideSingleMatchesScalarFormula(length);
tests.MaxSingleMatchesRuntimeFormula(length);
tests.MultiplySingleMatchesScalarFormula(length);
tests.NormalizeMatchesScalarFormula(length);
}
tests.ClampSinglePreservesRuntimeSpecialValueSemantics();
tests.ClampDoublePreservesRuntimeSpecialValueSemantics();
}
/// <summary> /// <summary>
/// Verifies that byte addition wraps modulo 256 and supports either input as the in-place destination. /// Verifies that byte addition wraps modulo 256 and supports either input as the in-place destination.
@ -153,6 +196,42 @@ public class TensorPrimitivesTests
AssertSingleBitsEqual(expected, source); AssertSingleBitsEqual(expected, source);
} }
/// <summary>
/// Verifies that double-precision negation preserves the scalar operator's exact bit-level behavior.
/// </summary>
/// <param name="length">The input length.</param>
[Theory]
[MemberData(nameof(SpanLengths))]
public void NegateDoubleMatchesScalarFormula(int length)
{
double[] values =
{
double.NaN,
-0D,
0D,
-1D,
1D,
double.NegativeInfinity,
double.PositiveInfinity
};
double[] source = new double[length];
double[] expected = new double[length];
for (int i = 0; i < source.Length; i++)
{
source[i] = values[i % values.Length];
expected[i] = -source[i];
}
double[] destination = new double[length];
TensorPrimitives_.Negate<double>(source, destination);
AssertDoubleBitsEqual(expected, destination);
TensorPrimitives_.Negate<double>(source, source);
AssertDoubleBitsEqual(expected, source);
}
/// <summary> /// <summary>
/// Verifies that integer clamping produces identical results for separate and in-place destinations. /// Verifies that integer clamping produces identical results for separate and in-place destinations.
/// </summary> /// </summary>

Loading…
Cancel
Save