// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System; using System.Diagnostics.Contracts; using System.Numerics; using System.Runtime.CompilerServices; namespace CommunityToolkit.HighPerformance.Helpers.Internals; /// /// Helpers to process sequences of values by reference. /// internal static partial class SpanHelper { /// /// Counts the number of occurrences of a given value into a target search space. /// /// A reference to the start of the search space. /// The number of items in the search space. /// The value to look for. /// The type of value to look for. /// The number of occurrences of in the search space [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static nint Count(ref T r0, nint length, T value) where T : IEquatable { if (!Vector.IsHardwareAccelerated) { return CountSequential(ref r0, length, value); } // Special vectorized version when using a supported type if (typeof(T) == typeof(byte) || typeof(T) == typeof(sbyte) || typeof(T) == typeof(bool)) { ref sbyte r1 = ref Unsafe.As(ref r0); sbyte target = Unsafe.As(ref value); return CountSimd(ref r1, length, target); } if (typeof(T) == typeof(char) || typeof(T) == typeof(ushort) || typeof(T) == typeof(short)) { ref short r1 = ref Unsafe.As(ref r0); short target = Unsafe.As(ref value); return CountSimd(ref r1, length, target); } if (typeof(T) == typeof(int) || typeof(T) == typeof(uint)) { ref int r1 = ref Unsafe.As(ref r0); int target = Unsafe.As(ref value); return CountSimd(ref r1, length, target); } if (typeof(T) == typeof(long) || typeof(T) == typeof(ulong)) { ref long r1 = ref Unsafe.As(ref r0); long target = Unsafe.As(ref value); return CountSimd(ref r1, length, target); } return CountSequential(ref r0, length, value); } /// /// Implements with a sequential search. /// [Pure] private static nint CountSequential(ref T r0, nint length, T value) where T : IEquatable { nint result = 0; nint offset = 0; // Main loop with 8 unrolled iterations while (length >= 8) { result += Unsafe.Add(ref r0, offset + 0).Equals(value).ToByte(); result += Unsafe.Add(ref r0, offset + 1).Equals(value).ToByte(); result += Unsafe.Add(ref r0, offset + 2).Equals(value).ToByte(); result += Unsafe.Add(ref r0, offset + 3).Equals(value).ToByte(); result += Unsafe.Add(ref r0, offset + 4).Equals(value).ToByte(); result += Unsafe.Add(ref r0, offset + 5).Equals(value).ToByte(); result += Unsafe.Add(ref r0, offset + 6).Equals(value).ToByte(); result += Unsafe.Add(ref r0, offset + 7).Equals(value).ToByte(); length -= 8; offset += 8; } if (length >= 4) { result += Unsafe.Add(ref r0, offset + 0).Equals(value).ToByte(); result += Unsafe.Add(ref r0, offset + 1).Equals(value).ToByte(); result += Unsafe.Add(ref r0, offset + 2).Equals(value).ToByte(); result += Unsafe.Add(ref r0, offset + 3).Equals(value).ToByte(); length -= 4; offset += 4; } // Iterate over the remaining values and count those that match while (length > 0) { result += Unsafe.Add(ref r0, offset).Equals(value).ToByte(); length -= 1; offset += 1; } return result; } /// /// Implements with a vectorized search. /// [Pure] private static nint CountSimd(ref T r0, nint length, T value) where T : unmanaged, IEquatable { nint result = 0; nint offset = 0; // Skip the initialization overhead if there are not enough items if (length >= Vector.Count) { Vector vc = new(value); do { // Calculate the maximum sequential area that can be processed in // one pass without the risk of numeric overflow in the dot product // to sum the partial results. We also backup the current offset to // be able to track how many items have been processed, which lets // us avoid updating a third counter (length) in the loop body. nint max = GetUpperBound(); nint chunkLength = length <= max ? length : max; nint initialOffset = offset; Vector partials = Vector.Zero; // Unrolled vectorized loop, with 8 unrolled iterations. We only run this when the // current type T is at least 2 bytes in size, otherwise the average chunk length // would always be too small to be able to trigger the unrolled loop, and the overall // performance would just be slightly worse due to the additional conditional branches. if (typeof(T) != typeof(sbyte)) { while (chunkLength >= Vector.Count * 8) { ref T ri0 = ref Unsafe.Add(ref r0, offset + (Vector.Count * 0)); Vector vi0 = Unsafe.As>(ref ri0); Vector ve0 = Vector.Equals(vi0, vc); partials -= ve0; ref T ri1 = ref Unsafe.Add(ref r0, offset + (Vector.Count * 1)); Vector vi1 = Unsafe.As>(ref ri1); Vector ve1 = Vector.Equals(vi1, vc); partials -= ve1; ref T ri2 = ref Unsafe.Add(ref r0, offset + (Vector.Count * 2)); Vector vi2 = Unsafe.As>(ref ri2); Vector ve2 = Vector.Equals(vi2, vc); partials -= ve2; ref T ri3 = ref Unsafe.Add(ref r0, offset + (Vector.Count * 3)); Vector vi3 = Unsafe.As>(ref ri3); Vector ve3 = Vector.Equals(vi3, vc); partials -= ve3; ref T ri4 = ref Unsafe.Add(ref r0, offset + (Vector.Count * 4)); Vector vi4 = Unsafe.As>(ref ri4); Vector ve4 = Vector.Equals(vi4, vc); partials -= ve4; ref T ri5 = ref Unsafe.Add(ref r0, offset + (Vector.Count * 5)); Vector vi5 = Unsafe.As>(ref ri5); Vector ve5 = Vector.Equals(vi5, vc); partials -= ve5; ref T ri6 = ref Unsafe.Add(ref r0, offset + (Vector.Count * 6)); Vector vi6 = Unsafe.As>(ref ri6); Vector ve6 = Vector.Equals(vi6, vc); partials -= ve6; ref T ri7 = ref Unsafe.Add(ref r0, offset + (Vector.Count * 7)); Vector vi7 = Unsafe.As>(ref ri7); Vector ve7 = Vector.Equals(vi7, vc); partials -= ve7; chunkLength -= Vector.Count * 8; offset += Vector.Count * 8; } } while (chunkLength >= Vector.Count) { ref T ri = ref Unsafe.Add(ref r0, offset); // Load the current Vector register, and then use // Vector.Equals to check for matches. This API sets the // values corresponding to matching pairs to all 1s. // Since the input type is guaranteed to always be signed, // this means that a value with all 1s represents -1, as // signed numbers are represented in two's complement. // So we can just subtract this intermediate value to the // partial results, which effectively sums 1 for each match. Vector vi = Unsafe.As>(ref ri); Vector ve = Vector.Equals(vi, vc); partials -= ve; chunkLength -= Vector.Count; offset += Vector.Count; } result += CastToNativeInt(Vector.Dot(partials, Vector.One)); length -= offset - initialOffset; } while (length >= Vector.Count); } // Optional 8 unrolled iterations. This is only done when a single SIMD // register can contain over 8 values of the current type, as otherwise // there could never be enough items left after the vectorized path if (Vector.Count > 8 && length >= 8) { result += Unsafe.Add(ref r0, offset + 0).Equals(value).ToByte(); result += Unsafe.Add(ref r0, offset + 1).Equals(value).ToByte(); result += Unsafe.Add(ref r0, offset + 2).Equals(value).ToByte(); result += Unsafe.Add(ref r0, offset + 3).Equals(value).ToByte(); result += Unsafe.Add(ref r0, offset + 4).Equals(value).ToByte(); result += Unsafe.Add(ref r0, offset + 5).Equals(value).ToByte(); result += Unsafe.Add(ref r0, offset + 6).Equals(value).ToByte(); result += Unsafe.Add(ref r0, offset + 7).Equals(value).ToByte(); length -= 8; offset += 8; } // Optional 4 unrolled iterations if (Vector.Count > 4 && length >= 4) { result += Unsafe.Add(ref r0, offset + 0).Equals(value).ToByte(); result += Unsafe.Add(ref r0, offset + 1).Equals(value).ToByte(); result += Unsafe.Add(ref r0, offset + 2).Equals(value).ToByte(); result += Unsafe.Add(ref r0, offset + 3).Equals(value).ToByte(); length -= 4; offset += 4; } // Iterate over the remaining values and count those that match while (length > 0) { result += Unsafe.Add(ref r0, offset).Equals(value).ToByte(); length -= 1; offset += 1; } return result; } /// /// Gets the upper bound for partial sums with a given parameter. /// /// The type argument currently in use. /// The native value representing the upper bound. [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] private static unsafe nint GetUpperBound() where T : unmanaged { if (typeof(T) == typeof(sbyte)) { return sbyte.MaxValue; } if (typeof(T) == typeof(short)) { return short.MaxValue; } if (typeof(T) == typeof(int)) { return int.MaxValue; } if (typeof(T) == typeof(long)) { if (sizeof(nint) == sizeof(int)) { return int.MaxValue; } // If we are on a 64 bit architecture and we are counting with a SIMD vector of 64 // bit values, we can use long.MaxValue as the upper bound, as a native integer will // be able to contain such a value with no overflows. This will allow the count tight // loop to process all the items in the target area in a single pass (except the mod). // The (void*) cast is necessary to ensure the right constant is produced on runtimes // before .NET 5 that don't natively support C# 9. For instance, removing that (void*) // cast results in the value 0xFFFFFFFFFFFFFFFF (-1) instead of 0x7FFFFFFFFFFFFFFFF. return (nint)(void*)long.MaxValue; } throw null!; } /// /// Casts a value of a given type to a native . /// /// The input type to cast. /// The input value to cast to native . /// The native cast of . [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] private static nint CastToNativeInt(T value) where T : unmanaged { if (typeof(T) == typeof(sbyte)) { return (byte)(sbyte)(object)value; } if (typeof(T) == typeof(short)) { return (ushort)(short)(object)value; } if (typeof(T) == typeof(int)) { return (nint)(uint)(int)(object)value; } if (typeof(T) == typeof(long)) { return (nint)(ulong)(long)(object)value; } throw null!; } }