diff --git a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/Common/TensorPrimitives.IBooleanUnaryOperator.cs b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/Common/TensorPrimitives.IBooleanUnaryOperator.cs
index 45c33f35e7c1ab..f1a7e5c978dd82 100644
--- a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/Common/TensorPrimitives.IBooleanUnaryOperator.cs
+++ b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/Common/TensorPrimitives.IBooleanUnaryOperator.cs
@@ -5,13 +5,19 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
+using System.Runtime.Intrinsics.X86;
namespace System.Numerics.Tensors
{
public static unsafe partial class TensorPrimitives
{
/// Unary operator that produces a Boolean result for each element.
- /// For vector-based methods, the Boolean result is either all-bits-set or zero.
+ ///
+ /// For vector-based methods, the Boolean result is either all-bits-set or zero.
+ /// An operator may also have a threshold form (): its result is then a single unsigned comparison of a
+ /// key derived from the element's bits against a constant, which lets fold a
+ /// block of vectors with one unsigned minimum or maximum per vector and compare once per block.
+ ///
private interface IBooleanUnaryOperator
{
static abstract bool Vectorizable { get; }
@@ -19,8 +25,89 @@ private interface IBooleanUnaryOperator
static abstract Vector128 Invoke(Vector128 x);
static abstract Vector256 Invoke(Vector256 x);
static abstract Vector512 Invoke(Vector512 x);
+
+ ///
+ /// Whether, reading the bits of the key and as unsigned integers of the element size, Invoke(x)
+ /// is Key(x) < ThresholdBits when and Key(x) > ThresholdBits otherwise.
+ ///
+ static virtual bool HasThresholdForm => false;
+
+ /// Whether the operator is true for keys below the threshold rather than for keys above it.
+ static virtual bool TrueBelowThreshold => throw new NotSupportedException();
+
+ /// The threshold of the threshold form, as the bits of an unsigned integer of the element size.
+ static virtual ulong ThresholdBits => throw new NotSupportedException();
+
+ /// The key of the threshold form: the element's bits, transformed so that the operator is a single comparison of them.
+ static virtual Vector128 Key(Vector128 x) => throw new NotSupportedException();
+ ///
+ static virtual Vector256 Key(Vector256 x) => throw new NotSupportedException();
+ ///
+ static virtual Vector512 Key(Vector512 x) => throw new NotSupportedException();
+ }
+
+ /// The bits of the positive infinity of , or .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static ulong PositiveInfinityBits()
+ {
+ Debug.Assert(typeof(T) == typeof(float) || typeof(T) == typeof(double));
+ return typeof(T) == typeof(float) ?
+ BitConverter.SingleToUInt32Bits(float.PositiveInfinity) :
+ BitConverter.DoubleToUInt64Bits(double.PositiveInfinity);
+ }
+
+ /// The bits of the smallest positive normal value of , or .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static ulong SmallestNormalBits()
+ {
+ Debug.Assert(typeof(T) == typeof(float) || typeof(T) == typeof(double));
+ return typeof(T) == typeof(float) ? 0x0080_0000u : 0x0010_0000_0000_0000ul;
+ }
+
+ /// The sign bit of , a primitive signed integer, or , as an unsigned integer of the element size.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static ulong SignBit() => 1ul << ((sizeof(T) * 8) - 1);
+
+ /// Subtracts from every element's bits, read as unsigned integers of the element size ( or ).
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static Vector128 SubtractBits(Vector128 x, ulong bits)
+ {
+ Debug.Assert(typeof(T) == typeof(float) || typeof(T) == typeof(double));
+ return typeof(T) == typeof(float) ?
+ (x.AsUInt32() - Vector128.Create((uint)bits)).As() :
+ (x.AsUInt64() - Vector128.Create(bits)).As();
+ }
+
+ /// Subtracts from every element's bits, read as unsigned integers of the element size ( or ).
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static Vector256 SubtractBits(Vector256 x, ulong bits)
+ {
+ Debug.Assert(typeof(T) == typeof(float) || typeof(T) == typeof(double));
+ return typeof(T) == typeof(float) ?
+ (x.AsUInt32() - Vector256.Create((uint)bits)).As() :
+ (x.AsUInt64() - Vector256.Create(bits)).As();
+ }
+
+ /// Subtracts from every element's bits, read as unsigned integers of the element size ( or ).
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static Vector512 SubtractBits(Vector512 x, ulong bits)
+ {
+ Debug.Assert(typeof(T) == typeof(float) || typeof(T) == typeof(double));
+ return typeof(T) == typeof(float) ?
+ (x.AsUInt32() - Vector512.Create((uint)bits)).As() :
+ (x.AsUInt64() - Vector512.Create(bits)).As();
}
+ ///
+ /// Combines the results of into an Any/All decision. The vectorized loops of
+ /// fold the per-vector results of a block of vectors into an accumulator
+ /// and decide once per block whether the block contains an element which settles the result, in which case they exit with
+ /// !DefaultResult. At 128 and 256 bits the accumulator is a result itself, the fold of the block's results with
+ /// Accumulate starting from the result that equals DefaultResult, so ShouldEarlyExit decides. At 512 bits
+ /// the operator's comparison leaves its result in a mask register, which a select consumes as it is (a masked blend) whereas a
+ /// bitwise operation would first have to expand the mask into a vector, so the accumulator starts from all bits set and
+ /// ClearSettled clears the lanes whose result settles the aggregation, which a zero lane then signals.
+ ///
private interface IAnyAllAggregator
{
static abstract bool DefaultResult { get; }
@@ -28,6 +115,14 @@ private interface IAnyAllAggregator
static abstract bool ShouldEarlyExit(Vector128 result);
static abstract bool ShouldEarlyExit(Vector256 result);
static abstract bool ShouldEarlyExit(Vector512 result);
+
+ /// Folds an operator result into the aggregation of the results folded so far, a result itself: the OR of Any results, the AND of All results.
+ static abstract Vector128 Accumulate(Vector128 accumulator, Vector128 result);
+ ///
+ static abstract Vector256 Accumulate(Vector256 accumulator, Vector256 result);
+
+ /// Clears the lanes of whose lane of settles the aggregation, that is, for which would hold.
+ static abstract Vector512 ClearSettled(Vector512 accumulator, Vector512 result);
}
private readonly struct AnyAggregator : IAnyAllAggregator
@@ -39,6 +134,13 @@ private interface IAnyAllAggregator
public static bool ShouldEarlyExit(Vector128 result) => Vector128.AnyWhereAllBitsSet(result);
public static bool ShouldEarlyExit(Vector256 result) => Vector256.AnyWhereAllBitsSet(result);
public static bool ShouldEarlyExit(Vector512 result) => Vector512.AnyWhereAllBitsSet(result);
+
+ public static Vector128 Accumulate(Vector128 accumulator, Vector128 result) => accumulator | result;
+ public static Vector256 Accumulate(Vector256 accumulator, Vector256 result) => accumulator | result;
+
+ // A lane where the operator was true is cleared: the select's constant is the zero vector, which costs no instruction,
+ // and the JIT folds the selection of the other lanes into a zero-masking move under the inverted comparison.
+ public static Vector512 ClearSettled(Vector512 accumulator, Vector512 result) => Vector512.ConditionalSelect(result, Vector512.Zero, accumulator);
}
private readonly struct AllAggregator : IAnyAllAggregator
@@ -47,22 +149,38 @@ private interface IAnyAllAggregator
public static bool ShouldEarlyExit(bool result) => !result;
- public static bool ShouldEarlyExit(Vector128 result) =>
- typeof(T) == typeof(float) ? Vector128.EqualsAny(result.AsUInt32(), Vector128.Zero) :
- typeof(T) == typeof(double) ? Vector128.EqualsAny(result.AsUInt64(), Vector128.Zero) :
- Vector128.EqualsAny(result, Vector128.Zero);
+ public static bool ShouldEarlyExit(Vector128 result) => AnyLaneZero(result);
+ public static bool ShouldEarlyExit(Vector256 result) => AnyLaneZero(result);
+ public static bool ShouldEarlyExit(Vector512 result) => AnyLaneZero(result);
- public static bool ShouldEarlyExit(Vector256 result) =>
- typeof(T) == typeof(float) ? Vector256.EqualsAny(result.AsUInt32(), Vector256.Zero) :
- typeof(T) == typeof(double) ? Vector256.EqualsAny(result.AsUInt64(), Vector256.Zero) :
- Vector256.EqualsAny(result, Vector256.Zero);
+ public static Vector128 Accumulate(Vector128 accumulator, Vector128 result) => accumulator & result;
+ public static Vector256 Accumulate(Vector256 accumulator, Vector256 result) => accumulator & result;
- public static bool ShouldEarlyExit(Vector512 result) =>
- typeof(T) == typeof(float) ? Vector512.EqualsAny(result.AsUInt32(), Vector512.Zero) :
- typeof(T) == typeof(double) ? Vector512.EqualsAny(result.AsUInt64(), Vector512.Zero) :
- Vector512.EqualsAny(result, Vector512.Zero);
+ // A lane where the operator was false (its result is zero) is cleared (see AnyAggregator).
+ public static Vector512 ClearSettled(Vector512 accumulator, Vector512 result) => Vector512.ConditionalSelect(result, accumulator, Vector512.Zero);
}
+ /// Whether any lane of is zero. For the floating-point types the lanes are compared as integers: a lane of an operator result or of an accumulator is either all bits set (a NaN) or zero.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static bool AnyLaneZero(Vector128 vector) =>
+ typeof(T) == typeof(float) ? Vector128.EqualsAny(vector.AsUInt32(), Vector128.Zero) :
+ typeof(T) == typeof(double) ? Vector128.EqualsAny(vector.AsUInt64(), Vector128.Zero) :
+ Vector128.EqualsAny(vector, Vector128.Zero);
+
+ /// Whether any lane of is zero. For the floating-point types the lanes are compared as integers: a lane of an operator result or of an accumulator is either all bits set (a NaN) or zero.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static bool AnyLaneZero(Vector256 vector) =>
+ typeof(T) == typeof(float) ? Vector256.EqualsAny(vector.AsUInt32(), Vector256.Zero) :
+ typeof(T) == typeof(double) ? Vector256.EqualsAny(vector.AsUInt64(), Vector256.Zero) :
+ Vector256.EqualsAny(vector, Vector256.Zero);
+
+ /// Whether any lane of is zero. For the floating-point types the lanes are compared as integers: a lane of an operator result or of an accumulator is either all bits set (a NaN) or zero.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static bool AnyLaneZero(Vector512 vector) =>
+ typeof(T) == typeof(float) ? Vector512.EqualsAny(vector.AsUInt32(), Vector512.Zero) :
+ typeof(T) == typeof(double) ? Vector512.EqualsAny(vector.AsUInt64(), Vector512.Zero) :
+ Vector512.EqualsAny(vector, Vector512.Zero);
+
private static bool All(ReadOnlySpan x)
where TOperator : struct, IBooleanUnaryOperator =>
AggregateAnyAll>(x);
@@ -71,110 +189,600 @@ private static bool Any(ReadOnlySpan x)
where TOperator : struct, IBooleanUnaryOperator =>
AggregateAnyAll>(x);
+ /// Vectors per block in the vectorized paths of . Must be even.
+ private const int AnyAllBlockVectors = 32;
+
private static bool AggregateAnyAll(ReadOnlySpan x)
where TOperator : struct, IBooleanUnaryOperator
where TAnyAll : struct, IAnyAllAggregator
{
Debug.Assert(!x.IsEmpty);
+ if (TOperator.HasThresholdForm)
+ {
+ // The keys are folded as unsigned integers of the element size. Their 64-bit minimum and maximum are single instructions
+ // only with AVX-512; elsewhere the operator's own comparison is cheaper.
+ if (sizeof(T) == 1) return AggregateAnyAllThreshold(x);
+ if (sizeof(T) == 2) return AggregateAnyAllThreshold(x);
+ if (sizeof(T) == 4) return AggregateAnyAllThreshold(x);
+ if (sizeof(T) == 8 && Avx512F.VL.IsSupported) return AggregateAnyAllThreshold(x);
+ }
+
+ if (Vector512.IsHardwareAccelerated && TOperator.Vectorizable && Vector512.IsSupported && x.Length >= Vector512.Count)
+ {
+ return AggregateAnyAllVectorized512(x);
+ }
+
+ if (Vector256.IsHardwareAccelerated && TOperator.Vectorizable && Vector256.IsSupported && x.Length >= Vector256.Count)
+ {
+ return AggregateAnyAllVectorized256(x);
+ }
+
+ if (Vector128.IsHardwareAccelerated && TOperator.Vectorizable && Vector128.IsSupported && x.Length >= Vector128.Count)
+ {
+ return AggregateAnyAllVectorized128(x);
+ }
+
+ return AggregateAnyAllScalar(x);
+ }
+
+ /// The scalar path of , used when vectorization is not supported or the input is too small to vectorize.
+ private static bool AggregateAnyAllScalar(ReadOnlySpan x)
+ where TOperator : struct, IBooleanUnaryOperator
+ where TAnyAll : struct, IAnyAllAggregator
+ {
+ ref T xRef = ref MemoryMarshal.GetReference(x);
+ for (int i = 0; i < x.Length; i++)
+ {
+ if (TAnyAll.ShouldEarlyExit(TOperator.Invoke(Unsafe.Add(ref xRef, i))))
+ {
+ return !TAnyAll.DefaultResult;
+ }
+ }
+
+ return TAnyAll.DefaultResult;
+ }
+
+ /// The 512-bit path of : the whole vectors in blocks, then one final vector that overlaps the last whole one.
+ ///
+ /// Every block of up to vectors is folded into two independent accumulators with no branch on the
+ /// data, and the exit decision is made once per block, so a hit is detected after at most one block of extra reads.
+ /// Blocks are visited in order and the whole input lies within the span, so the result is the same as with a test per vector.
+ /// The accumulators start from all bits set and have the lanes of the settling results cleared (see ).
+ ///
+ [MethodImpl(MethodImplOptions.NoInlining)] // called once per aggregation; its own inlining budget keeps the operator and the aggregator inlined
+ private static bool AggregateAnyAllVectorized512(ReadOnlySpan x)
+ where TOperator : struct, IBooleanUnaryOperator
+ where TAnyAll : struct, IAnyAllAggregator
+ {
+ Debug.Assert(Vector512.IsHardwareAccelerated && TOperator.Vectorizable && Vector512.IsSupported);
+ Debug.Assert(x.Length >= Vector512.Count);
+
ref T xRef = ref MemoryMarshal.GetReference(x);
- int i = 0, oneVectorFromEnd;
+ nuint length = (uint)x.Length;
+ nuint oneVectorFromEnd = length - (uint)Vector512.Count;
+ nuint i = 0;
- if (Vector512.IsHardwareAccelerated && TOperator.Vectorizable && Vector512.IsSupported)
+ // Whole blocks: two accumulators, one decision per block.
+ nuint blockLength = (uint)(AnyAllBlockVectors * Vector512.Count);
+ if (length >= blockLength)
{
- oneVectorFromEnd = x.Length - Vector512.Count;
- if (i <= oneVectorFromEnd)
+ nuint oneBlockFromEnd = length - blockLength;
+ do
{
- // Loop handling one vector at a time.
+ Vector512 accumulator0 = Vector512.AllBitsSet;
+ Vector512 accumulator1 = Vector512.AllBitsSet;
+ nuint blockEnd = i + blockLength;
do
{
- if (TAnyAll.ShouldEarlyExit(TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, (uint)i))))
- {
- return !TAnyAll.DefaultResult;
- }
-
- i += Vector512.Count;
+ accumulator0 = TAnyAll.ClearSettled(accumulator0, TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, i)));
+ accumulator1 = TAnyAll.ClearSettled(accumulator1, TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, i + (uint)Vector512.Count)));
+ i += (uint)(2 * Vector512.Count);
}
- while (i <= oneVectorFromEnd);
+ while (i < blockEnd);
- // Handle any remaining elements with a final vector.
- if (i != x.Length &&
- TAnyAll.ShouldEarlyExit(TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, (uint)(x.Length - Vector512.Count)))))
+ if (AnyLaneZero(accumulator0 & accumulator1))
{
return !TAnyAll.DefaultResult;
}
+ }
+ while (i <= oneBlockFromEnd);
+ }
- return TAnyAll.DefaultResult;
+ // The remaining whole vectors, fewer than a block.
+ if (i <= oneVectorFromEnd)
+ {
+ Vector512 accumulator = Vector512.AllBitsSet;
+ do
+ {
+ accumulator = TAnyAll.ClearSettled(accumulator, TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, i)));
+ i += (uint)Vector512.Count;
}
+ while (i <= oneVectorFromEnd);
+
+ if (AnyLaneZero(accumulator))
+ {
+ return !TAnyAll.DefaultResult;
+ }
+ }
+
+ // Handle any remaining elements with a final vector.
+ if (i != length &&
+ TAnyAll.ShouldEarlyExit(TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, oneVectorFromEnd))))
+ {
+ return !TAnyAll.DefaultResult;
}
- if (Vector256.IsHardwareAccelerated && TOperator.Vectorizable && Vector256.IsSupported)
+ return TAnyAll.DefaultResult;
+ }
+
+ /// The 256-bit path of : the whole vectors in blocks, then one final vector that overlaps the last whole one.
+ ///
+ /// Every block of up to vectors is folded into two independent accumulators with no branch on the
+ /// data, and the exit decision is made once per block, so a hit is detected after at most one block of extra reads.
+ /// Blocks are visited in order and the whole input lies within the span, so the result is the same as with a test per vector.
+ /// The accumulators are results themselves: the fold of the block's results, starting from the result that equals the default
+ /// (see ).
+ ///
+ [MethodImpl(MethodImplOptions.NoInlining)] // called once per aggregation; its own inlining budget keeps the operator and the aggregator inlined
+ private static bool AggregateAnyAllVectorized256(ReadOnlySpan x)
+ where TOperator : struct, IBooleanUnaryOperator
+ where TAnyAll : struct, IAnyAllAggregator
+ {
+ Debug.Assert(Vector256.IsHardwareAccelerated && TOperator.Vectorizable && Vector256.IsSupported);
+ Debug.Assert(x.Length >= Vector256.Count);
+
+ ref T xRef = ref MemoryMarshal.GetReference(x);
+ nuint length = (uint)x.Length;
+ nuint oneVectorFromEnd = length - (uint)Vector256.Count;
+ nuint i = 0;
+
+ Vector256 defaultResult = TAnyAll.DefaultResult ? Vector256.AllBitsSet : Vector256.Zero;
+
+ // Whole blocks: two accumulators, one decision per block.
+ nuint blockLength = (uint)(AnyAllBlockVectors * Vector256.Count);
+ if (length >= blockLength)
{
- oneVectorFromEnd = x.Length - Vector256.Count;
- if (i <= oneVectorFromEnd)
+ nuint oneBlockFromEnd = length - blockLength;
+ do
{
- // Loop handling one vector at a time.
+ Vector256 accumulator0 = defaultResult;
+ Vector256 accumulator1 = defaultResult;
+ nuint blockEnd = i + blockLength;
do
{
- if (TAnyAll.ShouldEarlyExit(TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, (uint)i))))
- {
- return !TAnyAll.DefaultResult;
- }
+ accumulator0 = TAnyAll.Accumulate(accumulator0, TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, i)));
+ accumulator1 = TAnyAll.Accumulate(accumulator1, TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, i + (uint)Vector256.Count)));
+ i += (uint)(2 * Vector256.Count);
+ }
+ while (i < blockEnd);
- i += Vector256.Count;
+ if (TAnyAll.ShouldEarlyExit(TAnyAll.Accumulate(accumulator0, accumulator1)))
+ {
+ return !TAnyAll.DefaultResult;
}
- while (i <= oneVectorFromEnd);
+ }
+ while (i <= oneBlockFromEnd);
+ }
- // Handle any remaining elements with a final vector.
- if (i != x.Length &&
- TAnyAll.ShouldEarlyExit(TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, (uint)(x.Length - Vector256.Count)))))
+ // The remaining whole vectors, fewer than a block.
+ if (i <= oneVectorFromEnd)
+ {
+ Vector256 accumulator = defaultResult;
+ do
+ {
+ accumulator = TAnyAll.Accumulate(accumulator, TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, i)));
+ i += (uint)Vector256.Count;
+ }
+ while (i <= oneVectorFromEnd);
+
+ if (TAnyAll.ShouldEarlyExit(accumulator))
+ {
+ return !TAnyAll.DefaultResult;
+ }
+ }
+
+ // Handle any remaining elements with a final vector.
+ if (i != length &&
+ TAnyAll.ShouldEarlyExit(TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, oneVectorFromEnd))))
+ {
+ return !TAnyAll.DefaultResult;
+ }
+
+ return TAnyAll.DefaultResult;
+ }
+
+ /// The 128-bit path of : the whole vectors in blocks, then one final vector that overlaps the last whole one.
+ ///
+ /// Every block of up to vectors is folded into two independent accumulators with no branch on the
+ /// data, and the exit decision is made once per block, so a hit is detected after at most one block of extra reads.
+ /// Blocks are visited in order and the whole input lies within the span, so the result is the same as with a test per vector.
+ /// The accumulators are results themselves: the fold of the block's results, starting from the result that equals the default
+ /// (see ).
+ ///
+ [MethodImpl(MethodImplOptions.NoInlining)] // called once per aggregation; its own inlining budget keeps the operator and the aggregator inlined
+ private static bool AggregateAnyAllVectorized128(ReadOnlySpan x)
+ where TOperator : struct, IBooleanUnaryOperator
+ where TAnyAll : struct, IAnyAllAggregator
+ {
+ Debug.Assert(Vector128.IsHardwareAccelerated && TOperator.Vectorizable && Vector128.IsSupported);
+ Debug.Assert(x.Length >= Vector128.Count);
+
+ ref T xRef = ref MemoryMarshal.GetReference(x);
+ nuint length = (uint)x.Length;
+ nuint oneVectorFromEnd = length - (uint)Vector128.Count;
+ nuint i = 0;
+
+ Vector128 defaultResult = TAnyAll.DefaultResult ? Vector128.AllBitsSet : Vector128.Zero;
+
+ // Whole blocks: two accumulators, one decision per block.
+ nuint blockLength = (uint)(AnyAllBlockVectors * Vector128.Count);
+ if (length >= blockLength)
+ {
+ nuint oneBlockFromEnd = length - blockLength;
+ do
+ {
+ Vector128 accumulator0 = defaultResult;
+ Vector128 accumulator1 = defaultResult;
+ nuint blockEnd = i + blockLength;
+ do
+ {
+ accumulator0 = TAnyAll.Accumulate(accumulator0, TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, i)));
+ accumulator1 = TAnyAll.Accumulate(accumulator1, TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, i + (uint)Vector128.Count)));
+ i += (uint)(2 * Vector128.Count);
+ }
+ while (i < blockEnd);
+
+ if (TAnyAll.ShouldEarlyExit(TAnyAll.Accumulate(accumulator0, accumulator1)))
{
return !TAnyAll.DefaultResult;
}
+ }
+ while (i <= oneBlockFromEnd);
+ }
- return TAnyAll.DefaultResult;
+ // The remaining whole vectors, fewer than a block.
+ if (i <= oneVectorFromEnd)
+ {
+ Vector128 accumulator = defaultResult;
+ do
+ {
+ accumulator = TAnyAll.Accumulate(accumulator, TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, i)));
+ i += (uint)Vector128.Count;
+ }
+ while (i <= oneVectorFromEnd);
+
+ if (TAnyAll.ShouldEarlyExit(accumulator))
+ {
+ return !TAnyAll.DefaultResult;
}
}
- if (Vector128.IsHardwareAccelerated && TOperator.Vectorizable && Vector128.IsSupported)
+ // Handle any remaining elements with a final vector.
+ if (i != length &&
+ TAnyAll.ShouldEarlyExit(TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, oneVectorFromEnd))))
+ {
+ return !TAnyAll.DefaultResult;
+ }
+
+ return TAnyAll.DefaultResult;
+ }
+
+ ///
+ /// for an operator with a threshold form, whose keys are folded as
+ /// , the unsigned integer of the element size.
+ ///
+ private static bool AggregateAnyAllThreshold(ReadOnlySpan x)
+ where TKey : unmanaged, IBinaryInteger
+ where TOperator : struct, IBooleanUnaryOperator
+ where TAnyAll : struct, IAnyAllAggregator
+ {
+ Debug.Assert(TOperator.HasThresholdForm);
+ Debug.Assert(sizeof(TKey) == sizeof(T));
+ Debug.Assert(!x.IsEmpty);
+
+ // The fold is chosen by type rather than by a branch on the direction so that the loops contain a single use of each key,
+ // which the JIT then folds into the fold instruction's memory operand.
+ return ThresholdFoldsMax() ?
+ AggregateAnyAllThreshold, TOperator, TAnyAll>(x) :
+ AggregateAnyAllThreshold, TOperator, TAnyAll>(x);
+ }
+
+ ///
+ /// Whether the threshold form of aggregated by folds the keys of a block
+ /// with their maximum rather than their minimum. Any looks for an element for which the operator holds and All for one for which it
+ /// does not: with the operator true below the threshold, those are a key below it (the minimum decides) and a key at or above it (the
+ /// maximum decides); with the operator true above the threshold, it is the other way round.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static bool ThresholdFoldsMax()
+ where TOperator : struct, IBooleanUnaryOperator
+ where TAnyAll : struct, IAnyAllAggregator =>
+ TOperator.TrueBelowThreshold == TAnyAll.DefaultResult;
+
+ ///
+ /// with the fold of the keys, , chosen:
+ /// or over (see ).
+ ///
+ private static bool AggregateAnyAllThreshold(ReadOnlySpan x)
+ where TKey : unmanaged, IBinaryInteger
+ where TFold : struct, IBinaryOperator
+ where TOperator : struct, IBooleanUnaryOperator
+ where TAnyAll : struct, IAnyAllAggregator
+ {
+ Debug.Assert(typeof(TFold) == (ThresholdFoldsMax() ? typeof(MaxOperator) : typeof(MinOperator)));
+
+ if (Vector512.IsHardwareAccelerated && TOperator.Vectorizable && Vector512.IsSupported && x.Length >= Vector512.Count)
+ {
+ return AggregateAnyAllThreshold512(x);
+ }
+
+ if (Vector256.IsHardwareAccelerated && TOperator.Vectorizable && Vector256.IsSupported && x.Length >= Vector256.Count)
+ {
+ return AggregateAnyAllThreshold256(x);
+ }
+
+ if (Vector128.IsHardwareAccelerated && TOperator.Vectorizable && Vector128.IsSupported && x.Length >= Vector128.Count)
{
- oneVectorFromEnd = x.Length - Vector128.Count;
- if (i <= oneVectorFromEnd)
+ return AggregateAnyAllThreshold128(x);
+ }
+
+ return AggregateAnyAllScalar(x);
+ }
+
+ /// The 512-bit path of : the whole vectors in blocks, then one final vector that overlaps the last whole one.
+ ///
+ /// The shape of , except that a block is folded with the unsigned
+ /// minimum or maximum of the elements' keys, one instruction per vector with no comparison, and the fold is compared against the
+ /// threshold once per block.
+ ///
+ [MethodImpl(MethodImplOptions.NoInlining)] // called once per aggregation; its own inlining budget keeps the operator inlined
+ private static bool AggregateAnyAllThreshold512(ReadOnlySpan x)
+ where TKey : unmanaged, IBinaryInteger
+ where TFold : struct, IBinaryOperator
+ where TOperator : struct, IBooleanUnaryOperator
+ where TAnyAll : struct, IAnyAllAggregator
+ {
+ Debug.Assert(Vector512.IsHardwareAccelerated && TOperator.Vectorizable && Vector512.IsSupported);
+ Debug.Assert(x.Length >= Vector512.Count);
+
+ ref T xRef = ref MemoryMarshal.GetReference(x);
+ nuint length = (uint)x.Length;
+ nuint oneVectorFromEnd = length - (uint)Vector512.Count;
+ nuint i = 0;
+
+ Vector512 identity = ThresholdFoldsMax() ? Vector512.Zero : Vector512.AllBitsSet;
+ Vector512 threshold = Vector512.Create(TKey.CreateTruncating(TOperator.ThresholdBits));
+
+ // Whole blocks: two accumulators, one decision per block.
+ nuint blockLength = (uint)(AnyAllBlockVectors * Vector512.Count);
+ if (length >= blockLength)
+ {
+ nuint oneBlockFromEnd = length - blockLength;
+ do
{
- // Loop handling one vector at a time.
+ Vector512 accumulator0 = identity;
+ Vector512 accumulator1 = identity;
+ nuint blockEnd = i + blockLength;
do
{
- if (TAnyAll.ShouldEarlyExit(TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, (uint)i))))
- {
- return !TAnyAll.DefaultResult;
- }
+ accumulator0 = TFold.Invoke(accumulator0, TOperator.Key(Vector512.LoadUnsafe(ref xRef, i)).As());
+ accumulator1 = TFold.Invoke(accumulator1, TOperator.Key(Vector512.LoadUnsafe(ref xRef, i + (uint)Vector512.Count)).As());
+ i += (uint)(2 * Vector512.Count);
+ }
+ while (i < blockEnd);
- i += Vector128.Count;
+ if (Settles(TFold.Invoke(accumulator0, accumulator1), threshold))
+ {
+ return !TAnyAll.DefaultResult;
}
- while (i <= oneVectorFromEnd);
+ }
+ while (i <= oneBlockFromEnd);
+ }
+
+ // The remaining whole vectors, fewer than a block.
+ if (i <= oneVectorFromEnd)
+ {
+ Vector512 accumulator = identity;
+ do
+ {
+ accumulator = TFold.Invoke(accumulator, TOperator.Key(Vector512.LoadUnsafe(ref xRef, i)).As());
+ i += (uint)Vector512.Count;
+ }
+ while (i <= oneVectorFromEnd);
- // Handle any remaining elements with a final vector.
- if (i != x.Length &&
- TAnyAll.ShouldEarlyExit(TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, (uint)(x.Length - Vector128.Count)))))
+ if (Settles(accumulator, threshold))
+ {
+ return !TAnyAll.DefaultResult;
+ }
+ }
+
+ // Handle any remaining elements with a final vector.
+ if (i != length &&
+ Settles(TOperator.Key(Vector512.LoadUnsafe(ref xRef, oneVectorFromEnd)).As(), threshold))
+ {
+ return !TAnyAll.DefaultResult;
+ }
+
+ return TAnyAll.DefaultResult;
+
+ // Whether the keys folded into the accumulator include one that settles the result: for Any a key on the operator's side of
+ // the threshold, for All a key on the other side (or on the threshold).
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ static bool Settles(Vector512 accumulator, Vector512