// Based on // https://github.com/CommunityToolkit/dotnet/blob/main/CommunityToolkit.HighPerformance/Extensions/ReadOnlySpanExtensions.cs // with additional methods and support for more types // 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 on https://github.com/CommunityToolkit/dotnet for more information. using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; namespace FisheryLib; [PublicAPI] #pragma warning disable CS8500 public static class ReadOnlySpanExtensions { /// /// Returns a reference to an element at a specified index within a given , with no bounds checks. /// /// The type of elements in the input instance. /// The input instance. /// The index of the element to retrieve within . /// A reference to the element within at the index specified by . /// This method doesn't do any bounds checks, therefore it is responsibility of the caller to ensure the parameter is valid. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ref T DangerousGetReferenceAt(this ReadOnlySpan span, int i) // Here we assume the input index will never be negative, so we do a (nint)(uint) cast // to force the JIT to skip the sign extension when going from int to native int. // On .NET Core 3.1, if we only use Unsafe.Add(ref r0, i), we get the following: // ============================= // L0000: mov rax, [rcx] // L0003: movsxd rdx, edx // L0006: lea rax, [rax+rdx*4] // L000a: ret // ============================= // Note the movsxd (move with sign extension) to expand the index passed in edx to // the whole rdx register. This is unnecessary and more expensive than just a mov, // which when done to a large register size automatically zeroes the upper bits. // With the (nint)(uint) cast, we get the following codegen instead: // ============================= // L0000: mov rax, [rcx] // L0003: mov edx, edx // L0005: lea rax, [rax+rdx*4] // L0009: ret // ============================= // Here we can see how the index is extended to a native integer with just a mov, // which effectively only zeroes the upper bits of the same register used as source. // These three casts are a bit verbose, but they do the trick on both 32 bit and 64 // bit architectures, producing optimal code in both cases (they are either completely // elided on 32 bit systems, or result in the correct register expansion when on 64 bit). // We first do an unchecked conversion to uint (which is just a reinterpret-cast). We // then cast to nint, so that we can obtain an IntPtr value without the range check (since // uint could be out of range there if the original index was negative). The final result // is a clean mov as shown above. This will eventually be natively supported by the JIT // compiler (see https://github.com/dotnet/runtime/issues/38794), but doing this here // still ensures the optimal codegen even on existing runtimes (eg. .NET Core 2.1 and 3.1). => ref Unsafe.Add(ref span.DangerousGetPinnableReference(), (nint)(uint)i); /// /// Returns a reference to an element at a specified index within a given , with no bounds checks. /// /// The type of elements in the input instance. /// The input instance. /// The index of the element to retrieve within . /// A reference to the element within at the index specified by . /// This method doesn't do any bounds checks, therefore it is responsibility of the caller to ensure the parameter is valid. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ref T DangerousGetReferenceAt(this ReadOnlySpan span, nint i) => ref Unsafe.Add(ref span.DangerousGetPinnableReference(), i); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static T First(this ReadOnlySpan span) => span[0]; [return: MaybeNull] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static T FirstOrDefault(this ReadOnlySpan span) => !span.IsEmpty ? span[0] : default; [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe TSource First(this ReadOnlySpan span, delegate* predicate) { ref var r0 = ref span.DangerousGetPinnableReference(); var length = (nint)(uint)span.Length; nint offset = 0; while (length >= 4) { if (predicate(Unsafe.Add(ref r0, offset))) goto returnForOffset0; if (predicate(Unsafe.Add(ref r0, offset + 1))) return Unsafe.Add(ref r0, offset + 1); if (predicate(Unsafe.Add(ref r0, offset + 2))) return Unsafe.Add(ref r0, offset + 2); if (predicate(Unsafe.Add(ref r0, offset + 3))) return Unsafe.Add(ref r0, offset + 3); length -= 4; offset += 4; } while (length > 0) { if (predicate(Unsafe.Add(ref r0, offset))) goto returnForOffset0; length--; offset++; } return ThrowNotFoundException(); returnForOffset0: return Unsafe.Add(ref r0, offset); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static TSource First(this ReadOnlySpan span, Predicate predicate) { ref var r0 = ref span.DangerousGetPinnableReference(); var length = (nint)(uint)span.Length; nint offset = 0; while (length >= 4) { if (predicate(Unsafe.Add(ref r0, offset))) goto returnForOffset0; if (predicate(Unsafe.Add(ref r0, offset + 1))) return Unsafe.Add(ref r0, offset + 1); if (predicate(Unsafe.Add(ref r0, offset + 2))) return Unsafe.Add(ref r0, offset + 2); if (predicate(Unsafe.Add(ref r0, offset + 3))) return Unsafe.Add(ref r0, offset + 3); length -= 4; offset += 4; } while (length > 0) { if (predicate(Unsafe.Add(ref r0, offset))) goto returnForOffset0; length--; offset++; } return ThrowNotFoundException(); returnForOffset0: return Unsafe.Add(ref r0, offset); } [DoesNotReturn] private static ref T ThrowNotFoundException() => throw new InvalidOperationException(); [return: MaybeNull] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe TSource FirstOrDefault(this ReadOnlySpan span, delegate* predicate) { ref var r0 = ref span.DangerousGetPinnableReference(); var length = (nint)(uint)span.Length; nint offset = 0; while (length >= 4) { if (predicate(Unsafe.Add(ref r0, offset))) goto returnForOffset0; if (predicate(Unsafe.Add(ref r0, offset + 1))) return Unsafe.Add(ref r0, offset + 1); if (predicate(Unsafe.Add(ref r0, offset + 2))) return Unsafe.Add(ref r0, offset + 2); if (predicate(Unsafe.Add(ref r0, offset + 3))) return Unsafe.Add(ref r0, offset + 3); length -= 4; offset += 4; } while (length > 0) { if (predicate(Unsafe.Add(ref r0, offset))) goto returnForOffset0; length--; offset++; } return default; returnForOffset0: return Unsafe.Add(ref r0, offset); } [return: MaybeNull] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static TSource FirstOrDefault(this ReadOnlySpan span, Predicate predicate) { ref var r0 = ref span.DangerousGetPinnableReference(); var length = (nint)(uint)span.Length; nint offset = 0; while (length >= 4) { if (predicate(Unsafe.Add(ref r0, offset))) goto returnForOffset0; if (predicate(Unsafe.Add(ref r0, offset + 1))) return Unsafe.Add(ref r0, offset + 1); if (predicate(Unsafe.Add(ref r0, offset + 2))) return Unsafe.Add(ref r0, offset + 2); if (predicate(Unsafe.Add(ref r0, offset + 3))) return Unsafe.Add(ref r0, offset + 3); length -= 4; offset += 4; } while (length > 0) { if (predicate(Unsafe.Add(ref r0, offset))) goto returnForOffset0; length--; offset++; } return default; returnForOffset0: return Unsafe.Add(ref r0, offset); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool Any(this ReadOnlySpan span) => !span.IsEmpty; [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe bool Any(this ReadOnlySpan span, delegate* predicate) { ref var r0 = ref span.DangerousGetPinnableReference(); var length = (nint)(uint)span.Length; nint offset = 0; while (length >= 4) { if (predicate(Unsafe.Add(ref r0, offset)) || predicate(Unsafe.Add(ref r0, offset + 1)) || predicate(Unsafe.Add(ref r0, offset + 2)) || predicate(Unsafe.Add(ref r0, offset + 3))) return true; length -= 4; offset += 4; } while (length > 0) { if (predicate(Unsafe.Add(ref r0, offset))) return true; length--; offset++; } return false; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool Any(this ReadOnlySpan span, Predicate predicate) { ref var r0 = ref span.DangerousGetPinnableReference(); var length = (nint)(uint)span.Length; nint offset = 0; while (length >= 4) { if (predicate(Unsafe.Add(ref r0, offset)) || predicate(Unsafe.Add(ref r0, offset + 1)) || predicate(Unsafe.Add(ref r0, offset + 2)) || predicate(Unsafe.Add(ref r0, offset + 3))) return true; length -= 4; offset += 4; } while (length > 0) { if (predicate(Unsafe.Add(ref r0, offset))) return true; length--; offset++; } return false; } /// /// Counts the number of occurrences of a given value into a target instance. /// /// The type of items in the input instance. /// The input instance to read. /// The value to look for. /// The number of occurrences of in . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int Count(this ReadOnlySpan span, T value) { ref var r0 = ref span.DangerousGetPinnableReference(); var length = (nint)(uint)span.Length; nint result = 0; nint offset = 0; // Main loop with 4 unrolled iterations while (length >= 4) { result += Unsafe.Add(ref r0, offset).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--; offset++; } return (int)result; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool Contains(this ReadOnlySpan span, T value) { ref var r0 = ref span.DangerousGetPinnableReference(); var length = (nint)(uint)span.Length; nint offset = 0; while (length >= 4) { if (Unsafe.Add(ref r0, offset).Equals(value) || Unsafe.Add(ref r0, offset + 1).Equals(value) || Unsafe.Add(ref r0, offset + 2).Equals(value) || Unsafe.Add(ref r0, offset + 3).Equals(value)) return true; length -= 4; offset += 4; } while (length > 0) { if (Unsafe.Add(ref r0, offset).Equals(value)) return true; length--; offset++; } return false; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int IndexOf(this ReadOnlySpan span, T value) { ref var r0 = ref span.DangerousGetPinnableReference(); var length = (nint)(uint)span.Length; nint offset = 0; while (length >= 4) { if (Unsafe.Add(ref r0, offset).Equals(value)) return (int)offset; if (Unsafe.Add(ref r0, offset + 1).Equals(value)) return (int)offset + 1; if (Unsafe.Add(ref r0, offset + 2).Equals(value)) return (int)offset + 2; if (Unsafe.Add(ref r0, offset + 3).Equals(value)) return (int)offset + 3; length -= 4; offset += 4; } while (length > 0) { if (Unsafe.Add(ref r0, offset).Equals(value)) return (int)offset; length--; offset++; } return -1; } /// /// Enumerates the items in the input instance, as pairs of reference/index values. /// This extension should be used directly within a loop: /// /// ReadOnlySpan<int> numbers = new[] { 1, 2, 3, 4, 5, 6, 7 }; /// /// foreach (var item in numbers.Enumerate()) /// { /// // Access the index and value of each item here... /// int index = item.Index; /// ref int value = ref item.Value; /// } /// /// The compiler will take care of properly setting up the loop with the type returned from this method. /// /// The type of items to enumerate. /// The source to enumerate. /// A wrapper type that will handle the reference/index enumeration for . /// The returned value shouldn't be used directly: use this extension in a loop. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ReadOnlySpanEnumerable Enumerate(this ReadOnlySpan span) => new(span); } public static class ReadOnlySpanReferenceExtensions { /// /// Gets the index of an element of a given from its reference. /// /// The type of items in the input . /// The input to calculate the index for. /// The reference to the target item to get the index for. /// The index of within , or -1. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe int IndexOf(this ReadOnlySpan span, ref T reference) { ref var r0 = ref span.DangerousGetPinnableReference(); var byteOffset = Unsafe.ByteOffset(ref r0, ref reference); var elementOffset = byteOffset / (nint)(uint)sizeof(T); return (nuint)elementOffset >= (uint)span.Length ? -1 : (int)elementOffset; } /// /// Determines whether a reference points to an element within the . /// /// The type of items in the input . /// The input to locate the reference in. /// The reference to the target item to locate. /// true if the reference points towards an element of ; otherwise, false. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool Contains(this ReadOnlySpan span, ref T reference) => span.IndexOf(ref reference) >= 0; } public static class ReadOnlySpanStructExtensions { /// /// Counts the number of occurrences of a given value into a target instance. /// /// The type of items in the input instance. /// The input instance to read. /// The value to look for. /// The number of occurrences of in . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int Count(this ReadOnlySpan span, in T value) where T : struct { ref var r0 = ref span.DangerousGetPinnableReference(); var length = (nint)(uint)span.Length; nint result = 0; nint offset = 0; // Main loop with 4 unrolled iterations while (length >= 4) { result += Unsafe.Add(ref r0, offset).Equals(ref Unsafe.AsRef(in value)).ToByte(); result += Unsafe.Add(ref r0, offset + 1).Equals(ref Unsafe.AsRef(in value)).ToByte(); result += Unsafe.Add(ref r0, offset + 2).Equals(ref Unsafe.AsRef(in value)).ToByte(); result += Unsafe.Add(ref r0, offset + 3).Equals(ref Unsafe.AsRef(in 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(ref Unsafe.AsRef(in value)).ToByte(); length--; offset++; } return (int)result; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool Contains(this ReadOnlySpan span, in T value) where T : struct { ref var r0 = ref span.DangerousGetPinnableReference(); var length = (nint)(uint)span.Length; nint offset = 0; while (length >= 4) { if (Unsafe.Add(ref r0, offset).Equals(ref Unsafe.AsRef(in value)) || Unsafe.Add(ref r0, offset + 1).Equals(ref Unsafe.AsRef(in value)) || Unsafe.Add(ref r0, offset + 2).Equals(ref Unsafe.AsRef(in value)) || Unsafe.Add(ref r0, offset + 3).Equals(ref Unsafe.AsRef(in value))) return true; length -= 4; offset += 4; } while (length > 0) { if (Unsafe.Add(ref r0, offset).Equals(ref Unsafe.AsRef(in value))) return true; length--; offset++; } return false; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int IndexOf(this ReadOnlySpan span, in T value) where T : struct { ref var r0 = ref span.DangerousGetPinnableReference(); var length = (nint)(uint)span.Length; nint offset = 0; while (length >= 4) { if (Unsafe.Add(ref r0, offset).Equals(ref Unsafe.AsRef(in value))) return (int)offset; if (Unsafe.Add(ref r0, offset + 1).Equals(ref Unsafe.AsRef(in value))) return (int)offset + 1; if (Unsafe.Add(ref r0, offset + 2).Equals(ref Unsafe.AsRef(in value))) return (int)offset + 2; if (Unsafe.Add(ref r0, offset + 3).Equals(ref Unsafe.AsRef(in value))) return (int)offset + 3; length -= 4; offset += 4; } while (length > 0) { if (Unsafe.Add(ref r0, offset).Equals(ref Unsafe.AsRef(in value))) return (int)offset; length--; offset++; } return -1; } } #pragma warning restore CS8500