File: Matching\ILEmitTrieFactory.cs
Web Access
Project: src\src\Http\Routing\src\Microsoft.AspNetCore.Routing.csproj (Microsoft.AspNetCore.Routing)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#nullable disable
 
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
 
namespace Microsoft.AspNetCore.Routing.Matching;
 
[RequiresDynamicCode("ILEmitTrieFactory uses runtime IL generation.")]
internal static class ILEmitTrieFactory
{
    // The algorthm we use only works for ASCII text. If we find non-ASCII text in the input
    // we need to reject it and let is be processed with a fallback technique.
    public const int NotAscii = int.MinValue;
 
    // Creates a Func of (string path, int start, int length) => destination
    // Not using PathSegment here because we don't want to mess with visibility checks and
    // generating IL without it is easier.
    public static Func<string, int, int, int> Create(
        int defaultDestination,
        int exitDestination,
        (string text, int destination)[] entries,
        bool? vectorize)
    {
        var method = new DynamicMethod(
            "GetDestination",
            typeof(int),
            new[] { typeof(string), typeof(int), typeof(int), });
 
        GenerateMethodBody(method.GetILGenerator(), defaultDestination, entries, vectorize);
 
#if IL_EMIT_SAVE_ASSEMBLY
            SaveAssembly(method.GetILGenerator(), defaultDestination, entries, vectorize);
#endif
 
        return (Func<string, int, int, int>)method.CreateDelegate(typeof(Func<string, int, int, int>));
    }
 
    // Internal for testing
    internal static bool ShouldVectorize((string text, int destination)[] entries)
    {
        // There's no value in vectorizing the computation if we're on 32bit or
        // if no string is long enough. We do the vectorized comparison with uint64 ulongs
        // which isn't beneficial if they don't map to the native size of the CPU. The
        // vectorized algorithm introduces additional overhead for casing.
 
        // Vectorize by default on 64bit (allow override for testing)
        return (IntPtr.Size == 8) &&
 
        // Don't vectorize if all of the strings are small (prevents allocating unused locals)
        entries.Any(e => e.text.Length >= 4);
    }
 
    private static void GenerateMethodBody(
        ILGenerator il,
        int defaultDestination,
        (string text, int destination)[] entries,
        bool? vectorize)
    {
        vectorize = vectorize ?? ShouldVectorize(entries);
 
        // See comments on Locals for details
        var locals = new Locals(il, vectorize.Value);
 
        // See comments on Labels for details
        var labels = new Labels()
        {
            ReturnDefault = il.DefineLabel(),
            ReturnNotAscii = il.DefineLabel(),
        };
 
        // See comments on Methods for details
        var methods = Methods.Instance;
 
        // Initializing top-level locals - this is similar to...
        // ReadOnlySpan<char> span = arg0.AsSpan(arg1, arg2);
        // ref byte p = ref Unsafe.As<char, byte>(MemoryMarshal.GetReference<char>(span))
 
        // arg0.AsSpan(arg1, arg2)
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Ldarg_1);
        il.Emit(OpCodes.Ldarg_2);
        il.Emit(OpCodes.Call, methods.AsSpan);
 
        // ReadOnlySpan<char> = ...
        il.Emit(OpCodes.Stloc, locals.Span);
 
        // MemoryMarshal.GetReference<char>(span)
        il.Emit(OpCodes.Ldloc, locals.Span);
        il.Emit(OpCodes.Call, methods.GetReference);
 
        // Unsafe.As<char, byte>(...)
        il.Emit(OpCodes.Call, methods.As);
 
        // ref byte p = ...
        il.Emit(OpCodes.Stloc_0, locals.P);
 
        const int binarySearchThreshold = 4; // The number of items above which it makes sense to binary search
        var groups = entries.GroupBy(e => e.text.Length).ToArray();
 
        if (groups.Length >= binarySearchThreshold)
        {
            // Only sort if binary search will be used.
            Array.Sort(groups, static (a, b) => a.Key.CompareTo(b.Key));
        }
 
        EmitIfLadder(groups);
 
        // Exit point - we end up here when the text doesn't match
        il.MarkLabel(labels.ReturnDefault);
        il.Emit(OpCodes.Ldc_I4, defaultDestination);
        il.Emit(OpCodes.Ret);
 
        // Exit point - we end up here with the text contains non-ASCII text
        il.MarkLabel(labels.ReturnNotAscii);
        il.Emit(OpCodes.Ldc_I4, NotAscii);
        il.Emit(OpCodes.Ret);
 
        void EmitIfLadder(Span<IGrouping<int, (string text, int destination)>> groups)
        {
            if (groups.Length < binarySearchThreshold)
            {
                // Use sequential if statements, starting from the most common length
                groups.Sort(static (a, b) => b.Count().CompareTo(a.Count()));
                for (var i = 0; i < groups.Length; i++)
                {
                    var group = groups[i];
 
                    // Similar to 'if (length != X) { ... }
                    var inside = il.DefineLabel();
                    var next = il.DefineLabel();
                    il.Emit(OpCodes.Ldarg_2);
                    il.Emit(OpCodes.Ldc_I4, group.Key);
                    il.Emit(OpCodes.Beq, inside);
                    il.Emit(OpCodes.Br, next);
 
                    // Process the group
                    il.MarkLabel(inside);
                    EmitTable(il, group.ToArray(), 0, group.Key, locals, labels, methods);
                    il.MarkLabel(next);
                }
            }
            else
            {
                // Use binary search tree
                var mid = groups.Length / 2;
 
                var rightBranch = il.DefineLabel();
                var next = il.DefineLabel();
 
                // if (length < X) { ... }
                il.Emit(OpCodes.Ldarg_2);
                il.Emit(OpCodes.Ldc_I4, groups[mid].Key);
                il.Emit(OpCodes.Bge, rightBranch);
 
                EmitIfLadder(groups[..mid]);
                il.Emit(OpCodes.Br, next);
 
                // else { ... }
                il.MarkLabel(rightBranch);
                EmitIfLadder(groups[mid..]);
 
                il.MarkLabel(next);
            }
        }
    }
 
    private static void EmitTable(
        ILGenerator il,
        (string text, int destination)[] entries,
        int index,
        int length,
        Locals locals,
        Labels labels,
        Methods methods)
    {
        // We've reached the end of the string.
        if (index == length)
        {
            EmitReturnDestination(il, entries);
            return;
        }
 
        // If 4 or more characters remain, and we're vectorizing, we should process 4 characters at a time.
        if (length - index >= 4 && locals.UInt64Value != null)
        {
            EmitVectorizedTable(il, entries, index, length, locals, labels, methods);
            return;
        }
 
        // Fall back to processing a character at a time.
        EmitSingleCharacterTable(il, entries, index, length, locals, labels, methods);
    }
 
    private static void EmitVectorizedTable(
        ILGenerator il,
        (string text, int destination)[] entries,
        int index,
        int length,
        Locals locals,
        Labels labels,
        Methods methods)
    {
        // Emits code similar to:
        //
        // uint64Value = Unsafe.ReadUnaligned<ulong>(ref p);
        // p = ref Unsafe.Add(ref p, 8);
        //
        // if ((uint64Value & ~0x007F007F007F007FUL) == 0)
        // {
        //     return NotAscii;
        // }
        // uint64LowerIndicator = value + (0x0080008000800080UL - 0x0041004100410041UL);
        // uint64UpperIndicator = value + (0x0080008000800080UL - 0x005B005B005B005BUL);
        // ulong temp1 = uint64LowerIndicator ^ uint64UpperIndicator
        // ulong temp2 = temp1 & 0x0080008000800080UL;
        // ulong temp3 = (temp2) >> 2;
        // uint64Value = uint64Value ^ temp3;
        //
        // This is a vectorized non-branching technique for processing 4 utf16 characters
        // at a time inside a single uint64.
        //
        // Similar to:
        // https://github.com/GrabYourPitchforks/coreclr/commit/a3c1df25c4225995ffd6b18fd0fc39d6b81fd6a5#diff-d89b6ca07ea349899e45eed5f688a7ebR81
        //
        // Basically we need to check if the text is non-ASCII first and bail if it is.
        // The rest of the steps will convert the text to lowercase by checking all characters
        // at a time to see if they are in the A-Z range, that's where 0x0041 and 0x005B come in.
 
        // IMPORTANT
        //
        // If you are modifying this code, be aware that the easiest way to make a mistake is by
        // getting the set of casts wrong doing something like:
        //
        // il.Emit(OpCodes.Ldc_I8, ~0x007F007F007F007FUL);
        //
        // The IL Emit apis don't have overloads that accept ulong or ushort, and will resolve
        // an overload that does an undesirable conversion (for instance converting ulong to float).
        //
        // IMPORTANT
 
        // Unsafe.ReadUnaligned<ulong>(ref p)
        il.Emit(OpCodes.Ldloc, locals.P);
        il.Emit(OpCodes.Call, methods.ReadUnalignedUInt64);
 
        // uint64Value = ...
        il.Emit(OpCodes.Stloc, locals.UInt64Value);
 
        // Unsafe.Add(ref p, 8)
        il.Emit(OpCodes.Ldloc, locals.P);
        il.Emit(OpCodes.Ldc_I4, 8); // 8 bytes were read
        il.Emit(OpCodes.Call, methods.Add);
 
        // p = ref ...
        il.Emit(OpCodes.Stloc, locals.P);
 
        // if ((uint64Value & ~0x007F007F007F007FUL) == 0)
        // {
        //     goto: NotAscii;
        // }
        il.Emit(OpCodes.Ldloc, locals.UInt64Value);
        il.Emit(OpCodes.Ldc_I8, unchecked((long)~0x007F007F007F007FUL));
        il.Emit(OpCodes.And);
        il.Emit(OpCodes.Brtrue, labels.ReturnNotAscii);
 
        if (entries.All(e => IsUInt64KeyAsciiLettersOnly(e.text, index)))
        {
            // Here we know that all characters in our keys will all be in the set [a-z]
            // If we set all the 0x20 bit in the input text, then it does not matter
            // if we are incorrectly changing e.g. @ to `, as it won't match our letters
            // anyway. In fact, since we know that the target set is [a-z] then the only
            // characters to match our target set after having their 0x20 bit set are...
            // [A-Z] which is exactly what we want to achieve.
 
            // uint64Value | 0x0020002000200020UL
            il.Emit(OpCodes.Ldloc, locals.UInt64Value);
            il.Emit(OpCodes.Ldc_I8, unchecked((long)0x0020002000200020UL));
            il.Emit(OpCodes.Or);
 
            // uint64Value = ...
            il.Emit(OpCodes.Stloc, locals.UInt64Value);
        }
        else
        {
            // uint64Value + (0x0080008000800080UL - 0x0041004100410041UL)
            il.Emit(OpCodes.Ldloc, locals.UInt64Value);
            il.Emit(OpCodes.Ldc_I8, unchecked((long)(0x0080008000800080UL - 0x0041004100410041UL)));
            il.Emit(OpCodes.Add);
 
            // uint64LowerIndicator = ...
            il.Emit(OpCodes.Stloc, locals.UInt64LowerIndicator);
 
            // value + (0x0080008000800080UL - 0x005B005B005B005BUL)
            il.Emit(OpCodes.Ldloc, locals.UInt64Value);
            il.Emit(OpCodes.Ldc_I8, unchecked((long)(0x0080008000800080UL - 0x005B005B005B005BUL)));
            il.Emit(OpCodes.Add);
 
            // uint64UpperIndicator = ...
            il.Emit(OpCodes.Stloc, locals.UInt64UpperIndicator);
 
            // uint64LowerIndicator ^ uint64UpperIndicator
            il.Emit(OpCodes.Ldloc, locals.UInt64LowerIndicator);
            il.Emit(OpCodes.Ldloc, locals.UInt64UpperIndicator);
            il.Emit(OpCodes.Xor);
 
            // ... & 0x0080008000800080UL
            il.Emit(OpCodes.Ldc_I8, unchecked((long)0x0080008000800080UL));
            il.Emit(OpCodes.And);
 
            // ... >> 2;
            il.Emit(OpCodes.Ldc_I4, 2);
            il.Emit(OpCodes.Shr_Un);
 
            // ...  ^ uint64Value
            il.Emit(OpCodes.Ldloc, locals.UInt64Value);
            il.Emit(OpCodes.Xor);
 
            // uint64Value = ...
            il.Emit(OpCodes.Stloc, locals.UInt64Value);
        }
 
        const int binarySearchThreshold = 4; // The number of items above which it makes sense to binary search
 
        var groups = entries.GroupBy(e => GetUInt64Key(e.text, index)).ToArray();
 
        if (groups.Length >= binarySearchThreshold)
        {
            // Only sort if binary search will be used.
            Array.Sort(groups, static (a, b) => unchecked((long)a.Key).CompareTo(unchecked((long)b.Key)));
        }
 
        EmitIfLadder(groups);
 
        // goto: defaultDestination
        il.Emit(OpCodes.Br, labels.ReturnDefault);
 
        void EmitIfLadder(Span<IGrouping<ulong, (string test, int destination)>> groups)
        {
            // Generate an 'if' ladder with an entry for each of the unique 64 bit sections of the text.
 
            if (groups.Length < binarySearchThreshold)
            {
                // Use sequential if statements, starting from the most common segment
                groups.Sort(static (a, b) => b.Count().CompareTo(a.Count()));
                foreach (var group in groups)
                {
                    // if (uint64Value == 0x.....) { ... }
                    var next = il.DefineLabel();
                    il.Emit(OpCodes.Ldloc, locals.UInt64Value);
                    il.Emit(OpCodes.Ldc_I8, unchecked((long)group.Key));
                    il.Emit(OpCodes.Bne_Un, next);
 
                    // Process the group
                    EmitTable(il, group.ToArray(), index + 4, length, locals, labels, methods);
                    il.MarkLabel(next);
                }
            }
            else
            {
                // Use binary search tree
                var mid = groups.Length / 2;
 
                var rightBranch = il.DefineLabel();
                var nextGroup = il.DefineLabel();
 
                // if (uint64Value < 0x.....) { ... }
                il.Emit(OpCodes.Ldloc, locals.UInt64Value);
                il.Emit(OpCodes.Ldc_I8, unchecked((long)groups[mid].Key));
                il.Emit(OpCodes.Bge_Un, rightBranch);
 
                EmitIfLadder(groups[..mid]);
                il.Emit(OpCodes.Br, nextGroup);
 
                // else { ... }
                il.MarkLabel(rightBranch);
                EmitIfLadder(groups[mid..]);
 
                il.MarkLabel(nextGroup);
            }
        }
    }
 
    private static void EmitSingleCharacterTable(
        ILGenerator il,
        (string text, int destination)[] entries,
        int index,
        int length,
        Locals locals,
        Labels labels,
        Methods methods)
    {
        // See the vectorized code path for a much more thorough explanation.
 
        // IMPORTANT
        //
        // If you are modifying this code, be aware that the easiest way to make a mistake is by
        // getting the set of casts wrong doing something like:
        //
        // il.Emit(OpCodes.Ldc_I4, ~0x007F);
        //
        // The IL Emit apis don't have overloads that accept ulong or ushort, and will resolve
        // an overload that does an undesirable conversion (for instance convering ulong to float).
        //
        // IMPORTANT
 
        // Unsafe.ReadUnaligned<ushort>(ref p)
        il.Emit(OpCodes.Ldloc, locals.P);
        il.Emit(OpCodes.Call, methods.ReadUnalignedUInt16);
 
        // uint16Value = ...
        il.Emit(OpCodes.Stloc, locals.UInt16Value);
 
        // Unsafe.Add(ref p, 2)
        il.Emit(OpCodes.Ldloc, locals.P);
        il.Emit(OpCodes.Ldc_I4, 2); // 2 bytes were read
        il.Emit(OpCodes.Call, methods.Add);
 
        // p = ref ...
        il.Emit(OpCodes.Stloc, locals.P);
 
        // if ((uint16Value & ~0x007FUL) == 0)
        // {
        //     goto: NotAscii;
        // }
        il.Emit(OpCodes.Ldloc, locals.UInt16Value);
        il.Emit(OpCodes.Ldc_I4, unchecked((int)((uint)~0x007F)));
        il.Emit(OpCodes.And);
        il.Emit(OpCodes.Brtrue, labels.ReturnNotAscii);
 
        // uint16Value | 0x20
        il.Emit(OpCodes.Ldloc, locals.UInt16Value);
        il.Emit(OpCodes.Ldc_I4, 0x20);
        il.Emit(OpCodes.Or);
 
        // uint16ValueLowerCase = ...
        il.Emit(OpCodes.Stloc, locals.UInt16ValueLowerCase);
 
        const int binarySearchThreshold = 4; // The number of items above which it makes sense to binary search
 
        // Now we generate an 'if' ladder with an entry for each of the unique
        // characters in the group.
        var groups = entries.GroupBy(e => GetUInt16Key(e.text, index)).ToArray();
 
        // We can still apply binary search in most cases. Specifically when besides letters,
        // there are no two keys which are equal aside from the 0x20 bit. Reasoning is that
        // if that's the case we can binary search on the (|0x20) version anyway and finally
        // compare equality with the correct version.
        var disableBinarySearch = groups.Any(group => groups.Any(otherGroup => otherGroup.Key != group.Key && (otherGroup.Key | 0x20) == (group.Key | 0x20)));
 
        if (!disableBinarySearch && groups.Length >= binarySearchThreshold)
        {
            // Only sort if binary search will be used. Sort by the "lower" value - even if not a letter
            // as with disableBinarySearch we have confirmed that no two keys will conflict.
            Array.Sort(groups, static (a, b) => (a.Key | 0x20).CompareTo(b.Key | 0x20));
        }
 
        EmitIfLadder(groups);
 
        // goto: defaultDestination
        il.Emit(OpCodes.Br, labels.ReturnDefault);
 
        void EmitIfLadder(Span<IGrouping<ushort, (string test, int destination)>> groups)
        {
            // Generate an 'if' ladder with an entry for each of the unique 64 bit sections of the text.
 
            if (disableBinarySearch || groups.Length < binarySearchThreshold)
            {
                // Use sequential if statements, starting from the most common segment
                groups.Sort(static (a, b) => b.Count().CompareTo(a.Count()));
 
                foreach (var group in groups)
                {
                    // Choose which variable against which to compare.
                    var comparisonLocal = group.Key >= 'a' && group.Key <= 'z'
                        ? locals.UInt16ValueLowerCase
                        : locals.UInt16Value;
 
                    // if (uint16Value/uint16ValueLowerCase == 'a') { ... }
                    var next = il.DefineLabel();
                    il.Emit(OpCodes.Ldloc, comparisonLocal);
                    il.Emit(OpCodes.Ldc_I4, unchecked((int)(uint)group.Key));
                    il.Emit(OpCodes.Ceq);
                    il.Emit(OpCodes.Brfalse, next);
 
                    // Process the group
                    EmitTable(il, group.ToArray(), index + 1, length, locals, labels, methods);
                    il.MarkLabel(next);
                }
            }
            else
            {
                // Use binary search tree
                var mid = groups.Length / 2;
 
                var rightBranch = il.DefineLabel();
                var nextGroup = il.DefineLabel();
 
                // if (uint16ValueLowerVase < 0x.....) { ... }
                il.Emit(OpCodes.Ldloc, locals.UInt16ValueLowerCase);
                il.Emit(OpCodes.Ldc_I4, unchecked(((int)(uint)groups[mid].Key | 0x20)));
                il.Emit(OpCodes.Bge_Un, rightBranch);
 
                EmitIfLadder(groups[..mid]);
                il.Emit(OpCodes.Br, nextGroup);
 
                // else { ... }
                il.MarkLabel(rightBranch);
                EmitIfLadder(groups[mid..]);
 
                il.MarkLabel(nextGroup);
            }
        }
    }
 
    public static void EmitReturnDestination(ILGenerator il, (string text, int destination)[] entries)
    {
        Debug.Assert(entries.Length == 1, "We should have a single entry");
        il.Emit(OpCodes.Ldc_I4, entries[0].destination);
        il.Emit(OpCodes.Ret);
    }
 
    /// <summary>
    /// Returns true if the key will only contains ascii letters
    /// </summary>
    private static bool IsUInt64KeyAsciiLettersOnly(string text, int index)
    {
        Debug.Assert(index + 4 <= text.Length);
        var span = text.AsSpan(index, 4);
        return char.IsAsciiLetter(span[0])
            && char.IsAsciiLetter(span[1])
            && char.IsAsciiLetter(span[2])
            && char.IsAsciiLetter(span[3]);
    }
 
    private static ulong GetUInt64Key(string text, int index)
    {
        Debug.Assert(index + 4 <= text.Length);
        var span = text.ToLowerInvariant().AsSpan(index);
        ref var p = ref Unsafe.As<char, byte>(ref MemoryMarshal.GetReference(span));
        return Unsafe.ReadUnaligned<ulong>(ref p);
    }
 
    private static ushort GetUInt16Key(string text, int index)
    {
        Debug.Assert(index + 1 <= text.Length);
        return (ushort)char.ToLowerInvariant(text[index]);
    }
 
    // We require a special build-time define since this is a testing/debugging
    // feature that will litter the app directory with assemblies.
#if IL_EMIT_SAVE_ASSEMBLY
        private static void SaveAssembly(
            int defaultDestination,
            int exitDestination,
            (string text, int destination)[] entries,
            bool? vectorize)
        {
            var assemblyName = "Microsoft.AspNetCore.Routing.ILEmitTrie" + DateTime.Now.Ticks;
            var fileName = assemblyName + ".dll";
            var assembly = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName(assemblyName), AssemblyBuilderAccess.RunAndSave);
            var module = assembly.DefineDynamicModule(assemblyName, fileName);
            var type = module.DefineType("ILEmitTrie");
            var method = type.DefineMethod(
                "GetDestination",
                MethodAttributes.Public | MethodAttributes.Static,
                CallingConventions.Standard,
                typeof(int),
                new [] { typeof(string), typeof(int), typeof(int), };
 
            GenerateMethodBody(method.GetILGenerator(), defaultDestination, exitDestination, entries, vectorize);
 
            type.CreateTypeInfo();
            assembly.Save(fileName);
        }
#endif
 
    private sealed class Locals
    {
        public Locals(ILGenerator il, bool vectorize)
        {
            P = il.DeclareLocal(typeof(byte).MakeByRefType());
            Span = il.DeclareLocal(typeof(ReadOnlySpan<char>));
 
            UInt16Value = il.DeclareLocal(typeof(ushort));
            UInt16ValueLowerCase = il.DeclareLocal(typeof(ushort));
 
            if (vectorize)
            {
                UInt64Value = il.DeclareLocal(typeof(ulong));
                UInt64LowerIndicator = il.DeclareLocal(typeof(ulong));
                UInt64UpperIndicator = il.DeclareLocal(typeof(ulong));
            }
        }
 
        /// <summary>
        /// Holds current character when processing a character at a time.
        /// </summary>
        public LocalBuilder UInt16Value { get; }
 
        /// <summary>
        /// Holds current character | 0x20 when processing a character at a time
        /// in binary search mode.
        /// </summary>
        public LocalBuilder UInt16ValueLowerCase { get; }
 
        /// <summary>
        /// Holds current character when processing 4 characters at a time.
        /// </summary>
        public LocalBuilder UInt64Value { get; }
 
        /// <summary>
        /// Used to covert casing. See comments where it's used.
        /// </summary>
        public LocalBuilder UInt64LowerIndicator { get; }
 
        /// <summary>
        /// Used to covert casing. See comments where it's used.
        /// </summary>
        public LocalBuilder UInt64UpperIndicator { get; }
 
        /// <summary>
        /// Holds a 'ref byte' reference to the current character (in bytes).
        /// </summary>
        public LocalBuilder P { get; }
 
        /// <summary>
        /// Holds the relevant portion of the path as a Span[byte].
        /// </summary>
        public LocalBuilder Span { get; }
    }
 
    private sealed class Labels
    {
        /// <summary>
        /// Label to goto that will return the default destination (not a match).
        /// </summary>
        public Label ReturnDefault { get; set; }
 
        /// <summary>
        /// Label to goto that will return a sentinel value for non-ascii text.
        /// </summary>
        public Label ReturnNotAscii { get; set; }
    }
 
    [RequiresDynamicCode("ILEmitTrieFactory uses runtime IL generation.")]
    private sealed class Methods
    {
        // Caching because the methods won't change, if we're being called once we're likely to
        // be called again.
        public static readonly Methods Instance = new Methods();
 
        private Methods()
        {
            Add = typeof(Unsafe).GetMethod(
                nameof(Unsafe.Add),
                genericParameterCount: 1,
                BindingFlags.Public | BindingFlags.Static,
                binder: null,
                types: new[] { Type.MakeGenericMethodParameter(0).MakeByRefType(), typeof(int), },
                modifiers: null)
                ?.MakeGenericMethod(typeof(byte));
 
            if (Add is null)
            {
                throw new InvalidOperationException("Failed to find Unsafe.Add{T}(ref T, int)");
            }
 
            As = typeof(Unsafe).GetMethod(
               nameof(Unsafe.As),
               genericParameterCount: 2,
               BindingFlags.Public | BindingFlags.Static,
               binder: null,
               types: new[] { Type.MakeGenericMethodParameter(0).MakeByRefType(), },
               modifiers: null)
               ?.MakeGenericMethod(typeof(char), typeof(byte));
 
            if (As is null)
            {
                throw new InvalidOperationException("Failed to find Unsafe.As{TFrom, TTo}(ref TFrom)");
            }
 
            AsSpan = typeof(MemoryExtensions).GetMethod(
                nameof(MemoryExtensions.AsSpan),
                BindingFlags.Public | BindingFlags.Static,
                binder: null,
                new[] { typeof(string), typeof(int), typeof(int), },
                modifiers: null);
            if (AsSpan == null)
            {
                throw new InvalidOperationException("Failed to find MemoryExtensions.AsSpan(string, int, int)");
            }
 
            GetReference = typeof(MemoryMarshal).GetMethod(
                nameof(MemoryMarshal.GetReference),
                genericParameterCount: 1,
                BindingFlags.Public | BindingFlags.Static,
                binder: null,
                types: new[] { typeof(ReadOnlySpan<>).MakeGenericType(Type.MakeGenericMethodParameter(0)), },
                modifiers: null)
                ?.MakeGenericMethod(typeof(char));
 
            if (GetReference == null)
            {
                throw new InvalidOperationException("Failed to find MemoryMarshal.GetReference{T}(ReadOnlySpan{T})");
            }
 
            ReadUnalignedUInt64 = typeof(Unsafe).GetMethod(
                nameof(Unsafe.ReadUnaligned),
                BindingFlags.Public | BindingFlags.Static,
                binder: null,
                new[] { typeof(byte).MakeByRefType(), },
                modifiers: null)
                .MakeGenericMethod(typeof(ulong));
            if (ReadUnalignedUInt64 == null)
            {
                throw new InvalidOperationException("Failed to find Unsafe.ReadUnaligned{T}(ref byte)");
            }
 
            ReadUnalignedUInt16 = typeof(Unsafe).GetMethod(
                nameof(Unsafe.ReadUnaligned),
                BindingFlags.Public | BindingFlags.Static,
                binder: null,
                new[] { typeof(byte).MakeByRefType(), },
                modifiers: null)
                .MakeGenericMethod(typeof(ushort));
            if (ReadUnalignedUInt16 == null)
            {
                throw new InvalidOperationException("Failed to find Unsafe.ReadUnaligned{T}(ref byte)");
            }
        }
 
        /// <summary>
        /// <see cref="Unsafe.Add{T}(ref T, int)"/> - Add[ref byte]
        /// </summary>
        public MethodInfo Add { get; }
 
        /// <summary>
        /// <see cref="Unsafe.As{TFrom, TTo}(ref TFrom)"/> - As[char, byte]
        /// </summary>
        public MethodInfo As { get; }
 
        /// <summary>
        /// <see cref="MemoryExtensions.AsSpan(string, int, int)"/>
        /// </summary>
        public MethodInfo AsSpan { get; }
 
        /// <summary>
        /// <see cref="MemoryMarshal.GetReference{T}(ReadOnlySpan{T})"/> - GetReference[char]
        /// </summary>
        public MethodInfo GetReference { get; }
 
        /// <summary>
        /// <see cref="Unsafe.ReadUnaligned{T}(ref readonly byte)"/> - ReadUnaligned[ulong]
        /// </summary>
        public MethodInfo ReadUnalignedUInt64 { get; }
 
        /// <summary>
        /// <see cref="Unsafe.ReadUnaligned{T}(ref readonly byte)"/> - ReadUnaligned[ushort]
        /// </summary>
        public MethodInfo ReadUnalignedUInt16 { get; }
    }
}