Unsafe.AsSpan:Span 如何替换指针?


C#是一种非常灵活的语言。 在它上面,您不仅可以编写后端或桌面应用程序。 我使用C#处理科学数据,这对使用该语言的工具提出了某些要求。 尽管netcore抓住了议程(考虑到在netstandard2.0之后,语言和运行时的大多数功能都不会netframeworknetframework ),但我还是继续处理旧项目。


在本文中,由于clr的特殊性,我考虑了Span<T>一种非显而易见(但可能是期望的)应用,以及netframeworknetcoreSpan<T>的实现之间的差异。


免责声明1

本文中的代码段决不打算用于实际项目中。


提出的(牵强?)问题的解决方案是一种概念证明。
无论如何,通过在项目中实施此操作,您将自担风险并承担风险。


免责声明2

我绝对确定,在某些情况下,这肯定会在膝盖处射杀某人。


C#类型安全旁路不太可能导致任何问题。


出于明显的原因,我并未在所有可能的情况下测试此代码,但是,初步结果看起来很有希望。


为什么我完全需要Span<T>


Spen允许您以更方便的形式使用unmanaged类型的数组,从而减少了必要的分配数量。 尽管几乎完全没有BCL netframework中的跨度支持,但是可以使用System.MemorySystem.BuffersSystem.Runtime.CompilerServices.Unsafe获得多种工具。
在我的遗留项目中,跨度的使用是有限的,但是,我发现它们是一个不明显的用法,同时会增加类型安全性。
这是什么应用程序? 在我的项目中,我使用从科学工具获得的数据。 这些是图像,通常是T[]的数组,其中T是非unmanaged基本类型之一,例如Int32 (aka int )。 为了正确地将这些映像序列化到磁盘,我需要支持1981年提出的极为不便的旧格式 ,此后几乎没有任何变化。 这种格式的主要问题是BigEndian 。 因此,为了写入(或读取) T[]的未压缩数组,您需要更改每个元素的字节序。 琐碎的任务。
有哪些明显的解决方案?


  1. 我们遍历数组T[] ,调用BitConverter.GetBytes(T) ,扩展这几个字节,复制到目标数组。
  2. 我们遍历数组T[] ,以new byte[] {(byte)((x & 0xFF00) >> 8), (byte)(x & 0x00FF)};的形式执行欺诈行为new byte[] {(byte)((x & 0xFF00) >> 8), (byte)(x & 0x00FF)}; (应在双字节类型上工作),写入目标数组。
  3. *但是T[]是数组吗? 元素是连续的,对不对? 因此,您可以进行所有操作,例如Buffer.BlockCopy(intArray, 0, byteArray, 0, intArray.Length * sizeof(int)); 。 该方法将数组复制到该数组,而忽略类型检查。 只需要不要错过界限和分配。 结果,我们混合了字节。
  4. *他们说C#(C++)++ 。 因此,启用/unsafefixed(int* p = &intArr[0]) byte* bPtr = (byte*)p; 现在您可以绕过源数组的字节表示形式,快速更改字节序并将块写入磁盘(添加stackalloc byte[]ArrayPool<byte>.Shared为中间缓冲区ArrayPool<byte>.Shared ),而无需为整个新的字节数组分配内存。

4点似乎可以解决所有问题,但是显式使用unsafe上下文和使用指针在某种程度上完全不同。 然后Span<T>助我们一臂之力。


Span<T>


Span<T>技术上讲, Span<T>应该提供用于处理内存图的工具,就像通过指针进行操作一样,同时无需“固定”内存中的数组。 具有数组边界的此类GC感知指针。 一切都很好,很安全。
一件事-尽管System.Runtime.CompilerServices.Unsafe丰富,但Span<T>被钉为Span<T>类型T 假设spen本质上是1 +长度的指针,那么如果您拉出指针,将其转换为另一种类型,重新计算长度并创建新的跨度该怎么办? 幸运的是,我们有public Span<T>(void* pointer, int length)
让我们编写一个简单的测试:


 [Test] public void Test() { void Flip(Span<byte> span) {/*   endianess */} Span<int> x = new [] {123}; Span<byte> y = DangerousCast<int, byte>(x); Assert.AreEqual(123, x[0]); Flip(y); Assert.AreNotEqual(123, x[0]); Flip(y); Assert.AreEqual(123, x[0]); } 

比我应该立即意识到这里有问题的高级开发人员。 测试会失败吗? 通常情况下,答案取决于
在这种情况下,它主要取决于运行时。 在netcore测试应该可以进行,但是在netframework ,测试如何netframework
有趣的是,如果您删除了一些论文,则该测试将在100%的情况下开始正常运行。
让我们做对。


1错了


正确答案:视情况而定


为什么结果取决于
让我们删除所有不必要的内容并在此处编写这样的代码:


 private static void Main() => Check(); private static void Check() { Span<int> x = new[] {999, 123, 11, -100}; Span<byte> y = As<int, byte>(ref x); Console.WriteLine(@"FRAMEWORK_NAME"); Write(ref x); Write(ref y); Console.WriteLine(); Write<int, int>(ref x, "Span<int> [0]"); Write<byte, int>(ref y, "Span<byte>[0]"); Console.WriteLine(); Write<int, int>(ref Offset<int, object>(ref x[0], 1), "Span<int> [0] offset by size_t"); Write<byte, int>(ref Offset<byte, object>(ref y[0], 1), "Span<byte>[0] offset by size_t"); Console.WriteLine(); GC.Collect(0, GCCollectionMode.Forced, true, true); Write<int, int>(ref x, "Span<int> [0] after GC"); Write<byte, int>(ref y, "Span<byte>[0] after GC"); Console.WriteLine(); Write(ref x); Write(ref y); } 

Write<T, U>方法接受类型T ,读取第一个元素的地址,并通过此指针读取一个类型U元素U 换句话说, Write<int, int>(ref x)将输出内存中的地址+数字999。
普通Write打印一个数组。
现在关于As<,>方法:


  private static unsafe Span<U> As<T, U>(ref Span<T> span) where T : unmanaged where U : unmanaged { fixed(T* ptr = span) return new Span<U>(ptr, span.Length * Unsafe.SizeOf<T>() / Unsafe.SizeOf<U>()); } 

C#语法现在通过隐式调用Span<T>.GetPinnableReference()方法来支持此fixed状态记录。
x64模式下的netframework4.8上运行此方法。 我们看看会发生什么:


 LEGACY [ 999, 123, 11, -100 ] [ 231, 3, 0, 0, 123, 0, 0, 0, 11, 0, 0, 0, 156, 255, 255, 255 ] 0x|00|00|02|8C|00|00|2F|B0 999 Span<int> [0] 0x|00|00|02|8C|00|00|2F|B0 999 Span<byte>[0] 0x|00|00|02|8C|00|00|2F|B8 11 Span<int> [0] offset by size_t 0x|00|00|02|8C|00|00|2F|B8 11 Span<byte>[0] offset by size_t 0x|00|00|02|8C|00|00|2B|18 999 Span<int> [0] after GC 0x|00|00|02|8C|00|00|2F|B0 6750318 Span<byte>[0] after GC [ 999, 123, 11, -100 ] [ 110, 0, 103, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] 

最初,两个跨度(尽管类型不同)的行为均相同,并且Span<byte>本质上表示原始数组的字节视图。 您需要什么。
好的,让我们尝试将跨度的开始位置更改为一个IntPtr的大小(或x64上的2 X int )并读取。 我们得到数组的第三个元素和正确的地址。 然后我们将收集垃圾...


 GC.Collect(0, GCCollectionMode.Forced, true, true); 

此方法中的最后一个标志要求GC压缩堆。 调用GC.Collect GC将移动原始本地数组。 Span<int>反映了这些更改,但是我们的Span<byte>继续指向旧地址,现在尚不清楚该地址。 一次拍摄自己所有膝盖的好方法!


现在,让我们看看在netcore3.0.100-preview8上调用的完全相同的代码片段的结果。


 CORE [ 999, 123, 11, -100 ] [ 231, 3, 0, 0, 123, 0, 0, 0, 11, 0, 0, 0, 156, 255, 255, 255 ] 0x|00|00|01|F2|8F|BD|C6|90 999 Span<int> [0] 0x|00|00|01|F2|8F|BD|C6|90 999 Span<byte>[0] 0x|00|00|01|F2|8F|BD|C6|98 11 Span<int> [0] offset by size_t 0x|00|00|01|F2|8F|BD|C6|98 11 Span<byte>[0] offset by size_t 0x|00|00|01|F2|8F|BD|BF|38 999 Span<int> [0] after GC 0x|00|00|01|F2|8F|BD|BF|38 999 Span<byte>[0] after GC [ 999, 123, 11, -100 ] [ 231, 3, 0, 0, 123, 0, 0, 0, 11, 0, 0, 0, 156, 255, 255, 255 ] 

据我所知,一切正常,并且运行稳定 。 压缩后,两个西班牙都更改其指针。 太好了! 但是,现在如何使其在遗留项目中起作用?


Jit内在


我完全忘记了对spans的支持是通过intrinsiknetcore实现的。 换句话说, netcore甚至可以创建指向数组片段的内部指针,并在GC移动它时正确更新链接。 在netframework ,跨度的nuget实现是拐杖。 实际上,我们有两种不同的用法:一种是从数组创建的,并跟踪其链接,第二种是从指针的,并且不知道其指向什么。 移动原始数组后,span指针将继续指向传递给其构造函数的指针所指向的位置。 为了进行比较,这是netcore中span的示例实现:


 readonly ref struct Span<T> where T : unmanaged { private readonly ByReference<T> _pointer; //  -   private readonly int _length; } 

和在netframework


 readonly ref struct Span<T> where T : unmanaged { private readonly Pinnable<T> _pinnable; private readonly IntPtr _byteOffset; private readonly int _length; } 

_pinnable包含对该数组的引用,如果将一个引用传递给构造函数,则_byteOffset包含一个移位(甚至整个数组的跨度也可能与数组在内存中的表示方式有关,具有一些非零移位)。 如果将void*指针传递给构造函数,则将其简单地转换为绝对_byteOffset 。 Span将被固定在内存区域,并且所有实例方法的条件都非常丰富,例如if(_pinnable is null) {/* */} else {/* _pinnable */} 。 在这种情况下该怎么办?


怎么做是不值得的,但我还是做了


本节专门介绍netframework支持的各种实现,这些实现允许netframework Span<T> -> Span<U> ,并保留所有必要的链接。
我警告您:这是异常编程的区域,可能存在基本错误,并且最终出现未定义的行为


方法1:天真


如示例所示,指针的转换将不会在netframework上提供所需的结果。 我们需要_pinnable值。 好的,我们将通过撤消私有字段来发现反射(非常糟糕,而且并非总是可能的),我们将其写成新的文字,我们会很高兴的。 只有一个问题:spen是一个ref struct ,它既不能是泛型参数,也不能打包到object 。 标准反射方法将需要一种或另一种方法来将跨度推入参考类型。 我没有找到简单的方法(甚至考虑了对私有领域的反思)。


方法2:我们需要更深入


我之前已经完成了所有工作( [1][2][3] )。 Spen是一种结构,无论T为何T三个字段占用相同的内存量( 在同一体系结构上 )。 如果[FieldOffset(0)]怎么办? 言归正传。


 [StructLayout(LayoutKind.Explicit)] ref struct Exchange<T, U> where T : unmanaged where U : unmanaged { [FieldOffset(0)] public Span<T> Span_1; [FieldOffset(0)] public Span<U> Span_2; } 

但是,当您启动程序时(或者,当尝试使用类型时), TypeLoadException遇到了TypeLoadException -泛型不能为LayoutKind.Explicit 。 好的,没关系,让我们走一条艰难的道路:


 [StructLayout(LayoutKind.Explicit)] public ref struct Exchange { [FieldOffset(0)] public Span<byte> ByteSpan; [FieldOffset(0)] public Span<sbyte> SByteSpan; [FieldOffset(0)] public Span<ushort> UShortSpan; [FieldOffset(0)] public Span<short> ShortSpan; [FieldOffset(0)] public Span<uint> UIntSpan; [FieldOffset(0)] public Span<int> IntSpan; [FieldOffset(0)] public Span<ulong> ULongSpan; [FieldOffset(0)] public Span<long> LongSpan; [FieldOffset(0)] public Span<float> FloatSpan; [FieldOffset(0)] public Span<double> DoubleSpan; [FieldOffset(0)] public Span<char> CharSpan; } 

现在您可以执行以下操作:


 private static Span<byte> As2(Span<int> span) { var exchange = new Exchange() { IntSpan = span }; return exchange.ByteSpan; } 

该方法仅解决一个问题_length字段按原样复制,因此在转换int > byte字节跨度比实际数组小4倍。
没问题:


 [StructLayout(LayoutKind.Sequential)] public ref struct Raw { public object Pinnable; public IntPtr Pointer; public int Length; } [StructLayout(LayoutKind.Explicit)] public ref struct Exchange { /* */ [FieldOffset(0)] public Raw RawView; } 

现在,通过RawView您可以访问每个单独的跨度字段。


 private static Span<byte> As2(Span<int> span) { var exchange = new Exchange() { IntSpan = span }; var exchange2 = new Exchange() { RawView = new Raw() { Pinnable = exchange.RawView.Pinnable, Pointer = exchange.RawView.Pointer, Length = exchange.RawView.Length * sizeof<int> / sizeof<byte> } }; return exchange2.ByteSpan; } 

如果您忽略使用肮脏的把戏,它就会按预期工作。 减号-无法创建转换器的通用版本,您必须对预定义类型感到满意。


方法3:疯狂


像任何普通的程序员一样,我喜欢使事情自动化。 为任何一对unmanaged类型编写转换器的需求并没有使我满意。 可以提供什么解决方案? 是的,让CLR 为您编写代码。


如何实现呢? 有不同的方式,有文章 。 简而言之,该过程如下所示:
创建一个构建构建器->创建一个模块构建器->构建一个类型-> {字段,方法等}->在输出中,我们得到Type的实例。
为了确切了解类型应该是什么样(这是一个ref struct ),我们使用任何ildasm类型的工具。 就我而言,它是dotPeek
创建一个类型生成器看起来像这样:


 var typeBuilder = _mBuilder.DefineType($"Generated_{typeof(T).Name}", TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.ExplicitLayout // <-    | TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit, typeof(ValueType)); 

现在是田野。 由于长度不同,我们无法将Span<T>直接复制到Span<U> ,因此我们需要为每种类型的演员创建​​两种类型


 [StructLayout(LayoutKind.Explicit)] ref struct Generated_Int32 { [FieldOffset(0)] public Span<Int32> Span; [FieldOffset(0)] public Raw Raw; } 

在这里Raw可以用手声明并重用。 不要忘记IsByRefLikeAttribute 。 使用字段,一切都很简单:


 var spanField = typeBuilder.DefineField("Span", typeof(Span<T>), FieldAttributes.Private); spanField.SetOffset(0); var rawField = typeBuilder.DefineField("Raw", typeof(Raw), FieldAttributes.Private); rawField.SetOffset(0); 

就是这样,最简单的类型已经准备就绪。 现在缓存程序集模块。 自定义类型被缓存在例如字典中( T -> Generated_{nameof(T)} )。 我们创建一个包装程序,根据两种类型的TInTOut生成两种类型的帮助程序并在跨度上执行必要的操作。 但是有一个。 就像反射的情况一样,几乎不可能在跨度(或其他ref struct )上使用它。 还是我没有找到简单的解决方案 。 怎么样


救援人员


反射方法通常看起来像这样:


  object Invoke(this MethodInfo mi, object @this, object[] otherArgs) 

它们不包含有关类型的信息,因此,如果您可以接受装箱(=包装),则不会有问题。
在我们的例子中, otherArgsotherArgs必须包含一个ref struct ,我无法绕开它。
但是,有一种更简单的方法。 假设一个类型具有getter和setter方法(不是属性,而是手动创建的简单方法)。
例如:


 void Generated_Int32.SetSpan(Span<Int32> span) => this.Span = span; 

除了方法之外,我们还可以声明一个委托类型(在代码中明确声明):


 delegate void SpanSetterDelegate<T>(Span<T> span) where T : unmanaged; 

我们必须这样做,因为标准动作必须具有Action<Span<T>>签名,但是不能将spenes用作通用参数。 但是, SpanSetterDelegate是绝对有效的委托。
创建必要的委托。 为此,请执行标准操作:


 var mi = type.GetMethod("Method_Name"); // ,    public & instance var spanSetter = (SpanSetterDelegate<T>) mi.CreateDelegate(typeof(SpanSetterDelegate<T>), @this); 

现在, spanSetter可以用作spanSetter(Span<T>.Empty); 。 至于@this 2 ,这是我们的动态类型的实例,当然是通过Activator.CreateInstance(type) ,因为该结构具有一个没有参数的默认构造函数。


因此,最后一个领域-我们需要动态生成方法。


2您可能会注意到这里出了点问题Activator.CreateInstance()打包ref struct实例。 请参阅下一节的结尾。


认识Reflection.Emit


我认为方法可以使用Expression生成,因为 简单的获取器/设置器的主体实际上包含几个表达式。 我选择了另一种更直接的方法。


如果您查看一个简单的getter的IL代码,则会看到类似( DebugX86netframework4.8 )的内容。


 nop ldarg.0 ldfld /* - */ stloc.0 br.s /*  */ ldloc.0 ret 

有很多地方可以停止和调试。
在发行版本中,只有最重要的部分保留:


 ldarg.0 ldfld /* - */ ret 

实例方法的null参数是... this 。 因此,以下是用IL编写的:
1)下载
2)加载字段值
3)带回来


只是吗 Reflection.Emit有一个特殊的重载,除了操作码之外,还需要一个字段描述符参数。 与我们之前收到的一样,例如spanField


 var getSpan = type.DefineMethod("GetSpan", MethodAttributes.Public | MethodAttributes.HideBySig, CallingConventions.Standard, typeof(Span<T>), Array.Empty<Type>()); gen = getSpan.GetILGenerator(); gen.Emit(OpCodes.Ldarg_0); gen.Emit(OpCodes.Ldfld, spanField); gen.Emit(OpCodes.Ret); 

对于setter来说,它有点复杂,您需要将其加载到堆栈上,加载函数的第一个参数,然后在字段中调用write指令,但不返回任何内容:


 ldarg.0 ldarg.1 stfld /*   */ ret 

Raw字段中完成此过程之后,声明了必要的委托(或使用标准委托),我们得到了一个动态类型和四个访问器方法,从中生成了正确的泛型委托。


我们编写一个包装器类,该包装器类使用两个泛型参数( TInTOut )接收引用相应(缓存的)动态类型的Type实例,此后,它创建每种类型的一个对象并生成四个泛型委托,即


  1. void SetSpan(Span<TIn> span)以将源范围写入结构
  2. Raw GetRaw()Raw结构读取跨度的内容
  3. void SetRaw(Raw raw)将修改后的Raw结构写入第二个对象
  4. Span<TOut> GetSpan()返回具有正确设置和重新计算的字段的所需类型的跨度。

有趣的是,动态类型实例需要创建一次。 创建委托时,对这些对象的引用将作为@this参数传递。 这违反了规则。 Activator.CreateInstance返回object 显然,这是由于动态类型本身未 type.IsByRef 类似 ref的事实( type.IsByRef Like == false ),但是可以创建 类似 ref的字段。 显然,这种限制存在于语言中,但CLR消化。 在非标准使用的情况下,膝盖可能会在这里开枪。 3


因此,我们得到了一个泛型类型的实例,该实例包含四个委托和两个对动态类实例的隐式引用。 连续执行相同的种姓时,可以重用委托和结构。 为了提高性能,我们再次缓存(已经是类型转换器)对(TIn, TOut) -> Generator<TIn, TOut>


笔触是最后一个:我们给出类型, Span<TIn> -> Span<TOut>


 public Span<TOut> Cast(Span<TIn> span) { //      if (span.IsEmpty) return Span<TOut>.Empty; // Caller   ,       if (span.Length * Unsafe.SizeOf<TIn>() % Unsafe.SizeOf<TOut>() != 0) throw new InvalidOperationException(); //      // Span<TIn> _input.Span = span; _spanSetter(span); //  Raw // Raw raw = _input.Raw; var raw = _rawGetter(); var newRaw = new Raw() { Pinnable = raw.Pinnable, //    Pinnable Pointer = raw.Pointer, //   Length = raw.Length * Unsafe.SizeOf<TIn>() / Unsafe.SizeOf<TOut>() //   }; //   Raw    // Raw _output.Raw = newRaw; _rawSetter(newRaw); //     // Span<TOut> _output.Span return _spanGetter(); } 

结论


有时出于体育兴趣的考虑,您可以绕过语言的某些限制并实现非标准功能。 当然,后果自负。 值得注意的是,动态方法允许您完全放弃指针和unsafe / fixed上下文,这可能是一个好处。 明显的缺点是需要反射和类型生成。


对于那些读到最后的人。


天真的基准测试结果

这一切有多快?
我在一个愚蠢的场景中比较了种姓的速度,这种场景不能反映出这种种姓和跨度的实际/潜在用途,但至少可以给出速度的概念。


  1. Cast_Explicit , 2 . ;
  2. Cast_IL 3 , Generator<TIn, TOut> , , ;
  3. Cast_IL_Cached Generator<TIn, TOut> , - , .. ;
  4. Buffer , , . .

int[N] N/2 .


, , . , . , , . , unmanaged .


 BenchmarkDotNet=v0.11.5, OS=Windows 10.0.18362 Intel Core i7-2700K CPU 3.50GHz (Sandy Bridge), 1 CPU, 8 logical and 4 physical cores [Host] : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.8.3815.0 Clr : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.8.3815.0 Job=Clr Runtime=Clr InvocationCount=1 UnrollFactor=1 

MethodñMeanErrorStdDevMedianRatioRatioSD
Cast_Explicit100362.2 ns18.0967 ns52.7888 ns400.0 ns1.000.00
Cast_IL1001,237.9 ns28.5954 ns67.4027 ns1,200.0 ns3.470.51
Cast_IL_Cached100522.8 ns25.2640 ns71.2576 ns500.0 ns1.460.27
Buffer100300.0 ns0.0000 ns0.0000 ns300.0 ns0.780.11
Cast_Explicit10002,628.6 ns54.0688 ns64.3650 ns2,600.0 ns1.000.00
Cast_IL10003,216.7 ns49.8568 ns38.9249 ns3,200.0 ns1.210.03
Cast_IL_Cached10002,484.6 ns44.9717 ns37.5534 ns2,500.0 ns0.940.02
Buffer10002,055.6 ns43.9695 ns73.4631 ns2,000.0 ns0.780.03
Cast_Explicit10000002,515,157.1 ns11,809.8538 ns10,469.1278 ns2,516,050.0 ns1.000.00
Cast_IL10000002,263,826.7 ns23,724.4930 ns22,191.9054 ns2,262,000.0 ns0.900.01
Cast_IL_Cached10000002,265,186.7 ns19,505.5913 ns18,245.5422 ns2,266,300.0 ns0.900.01
Buffer10000001,959,547.8 ns39,175.7435 ns49,544.7719 ns1,959,200.0 ns0.780.02
Cast_Explicit100000000255,751,392.9 ns2,595,107.7066 ns2,300,495.3873 ns255,298,950.0 ns1.000.00
Cast_IL100000000228,709,457.1 ns527,430.9293 ns467,553.7809 ns228,864,100.0 ns0.890.01
Cast_IL_Cached100000000227,966,553.8 ns355,027.3545 ns296,463.9203 ns227,903,600.0 ns0.890.01
Buffer100000000213,216,776.9 ns1,198,565.1142 ns1,000,856.1536 ns213,517,800.0 ns0.830.01

Acknowledgements

JetBrains ( :-)) R# VS standalone- dotPeek , . BenchmarkDotNet BenchmarkDotNet, youtube- NDC Conferences DotNext , , .


聚苯乙烯


3 , ref , , . ( ) . ref structs,


 static Raw Generated_Int32.GetRaw(Span<int> span) { var inst = new Generated_Int32() { Span = span }; return inst.Raw; } 

, Reflection.Emit . , ILGenerator.DeclareLocal .


 static Span<int> Generated_Int32.GetSpan(Raw raw); 


 delegate Raw GetRaw<T>(Span<T> span) where T : unmanaged; delegate Span<T> GetSpan<T>(Raw raw) where T : unmanaged; 

, , ref — . 因为 ,


 var getter = type.GetMethod(@"GetRaw", BindingFlags.Static | BindingFlags.Public).CreateDelegate(typeof(GetRaw<T>), null) as GetRaw<T>; 


 Raw raw = getter(Span<TIn>.Empty); Raw newRaw = convert(raw); Span<TOut> = setter(newRaw); 

UPD01:

Source: https://habr.com/ru/post/zh-CN465077/


All Articles