Inseguro.AsSpan: Span ¿Cómo reemplazar los punteros?


C# es un lenguaje increíblemente flexible. En él puede escribir no solo el backend o las aplicaciones de escritorio. Utilizo C# para trabajar con datos científicos, que imponen ciertos requisitos sobre las herramientas disponibles en el lenguaje. Aunque netcore toma el orden del día (teniendo en cuenta que después de netstandard2.0 mayoría de las características de los dos idiomas y el tiempo de ejecución no son netframework con netframework ), sigo trabajando con proyectos netframework .


En este artículo, considero una aplicación no obvia (¿pero probablemente deseada?) De Span<T> y la diferencia entre la implementación de Span<T> en netframework y netcore debido a los detalles de clr .


Descargo de responsabilidad 1

Los fragmentos de código de este artículo de ninguna manera están destinados a su uso en proyectos del mundo real.


La solución propuesta para el problema (¿exagerado?) Es más bien una prueba de concepto.
En cualquier caso, al implementar esto en su proyecto, lo hace bajo su propio riesgo y riesgo.


Descargo de responsabilidad 2

Estoy absolutamente seguro de que en algún lugar, en algún caso, esto definitivamente disparará a alguien en la rodilla.


El bypass de seguridad de tipo en C# poco probable que conduzca a algo bueno.


Por razones obvias, no probé este código en todas las situaciones posibles, sin embargo, los resultados preliminares parecen prometedores.


¿Por qué necesito Span<T> ?


Spen le permite trabajar con matrices de tipos unmanaged en una forma más conveniente, reduciendo el número de asignaciones necesarias. A pesar de que el soporte de span en el netframework BCL netframework casi ausente, se pueden obtener varias herramientas usando System.Memory , System.Buffers y System.Runtime.CompilerServices.Unsafe .
El uso de vanos en mi proyecto heredado es limitado, sin embargo, me pareció un uso no obvio, mientras escupía en seguridad de tipo.
¿Qué es esta aplicación? En mi proyecto trabajo con datos obtenidos de una herramienta científica. Estas son imágenes que, en general, son una matriz de T[] , donde T es uno de los tipos primitivos unmanaged , por ejemplo Int32 (también conocido como int ). Para serializar correctamente estas imágenes en el disco, necesito admitir el formato heredado increíblemente inconveniente, que se propuso en 1981 , y desde entonces ha cambiado poco. El principal problema de este formato es que es BigEndian . Por lo tanto, para escribir (o leer) una matriz sin comprimir de T[] , debe cambiar la endianess de cada elemento. La tarea trivial.
¿Cuáles son algunas soluciones obvias?


  1. Repetimos la matriz T[] , llamamos a BitConverter.GetBytes(T) , expandimos estos pocos bytes, copiamos a la matriz de destino.
  2. Repetimos la matriz T[] , realizamos fraudes de la forma new byte[] {(byte)((x & 0xFF00) >> 8), (byte)(x & 0x00FF)}; (debería funcionar en tipos de doble byte), escriba en la matriz de destino.
  3. * ¿ Pero es T[] una matriz? Los elementos están en una fila, ¿verdad? Entonces puede ir hasta el final, por ejemplo, Buffer.BlockCopy(intArray, 0, byteArray, 0, intArray.Length * sizeof(int)); . El método copia la matriz a la matriz ignorando la comprobación de tipo. Solo es necesario no perderse los límites y la asignación. Mezclamos los bytes como resultado.
  4. * Dicen que C# es (C++)++ . Por lo tanto, active /unsafe , fixed(int* p = &intArr[0]) byte* bPtr = (byte*)p; y ahora puede ejecutar la representación de bytes de la matriz de origen, cambiar la endianess sobre la marcha y escribir bloques en el disco (agregando stackalloc byte[] o ArrayPool<byte>.Shared para el búfer intermedio) sin asignar memoria para una matriz de bytes completamente nueva.

Parece que el punto 4 le permite resolver todos los problemas, pero el uso explícito del contexto unsafe y el trabajo con punteros es de alguna manera completamente diferente. Entonces Span<T> viene en nuestra ayuda.


Span<T>


Span<T> debería proporcionar técnicamente herramientas para trabajar con gráficos de memoria casi como trabajar con punteros, al tiempo que elimina la necesidad de "arreglar" la matriz en la memoria. Tal puntero compatible con GC con límites de matriz. Todo está bien y seguro.
Una cosa, pero a pesar de la riqueza de System.Runtime.CompilerServices.Unsafe , Span<T> clavado en el tipo T Dado que spen es esencialmente un puntero de longitud 1 +, ¿qué sucede si saca su puntero, lo convierte a otro tipo, recalcula la longitud y crea un nuevo tramo? Afortunadamente, tenemos public Span<T>(void* pointer, int length) .
Escribamos una prueba simple:


 [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]); } 

Desarrolladores más avanzados de los que debería darme cuenta inmediatamente de lo que está mal aquí. ¿La prueba fallará? La respuesta, como suele suceder, depende .
En este caso, depende principalmente del tiempo de ejecución. En netcore prueba debería funcionar, pero en netframework , cómo funciona.
Curiosamente, si elimina algunos de los ensayos, la prueba comienza a funcionar correctamente en el 100% de los casos.
Vamos a hacerlo bien.


1 Estaba equivocado .


Respuesta correcta: depende


¿Por qué depende el resultado?
Eliminemos todo lo innecesario y escriba aquí dicho código:


 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); } 

El método Write<T, U> acepta un intervalo de tipo T , lee la dirección del primer elemento y lee a través de este puntero un elemento de tipo U En otras palabras, Write<int, int>(ref x) generará la dirección en la memoria + el número 999.
Normal Write imprime una matriz.
Ahora sobre el método 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# sintaxis de C# ahora admite este registro de estado Span<T>.GetPinnableReference() al llamar implícitamente al método Span<T>.GetPinnableReference() .
Ejecute este método en netframework4.8 en modo x64 . Nos fijamos en lo que sucede:


 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 ] 

Inicialmente, ambos tramos (a pesar de los diferentes tipos) se comportan de manera idéntica, y el Span<byte> , en esencia, representa una vista de bytes de la matriz original. Lo que necesitas
Bien, intentemos cambiar el comienzo del lapso al tamaño de un IntPtr (o 2 X int en x64 ) y leer. Obtenemos el tercer elemento de la matriz y la dirección correcta. Y luego recogeremos la basura ...


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

El último indicador en este método le pide al GC compacte el montón. Después de llamar a GC.Collect GC mueve la matriz local original. Span<int> refleja estos cambios, pero nuestro Span<byte> continúa apuntando a la dirección anterior, donde ahora no está claro qué. ¡Una excelente manera de dispararte todas las rodillas a la vez!


Ahora veamos el resultado del mismo fragmento de código llamado en 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 ] 

Todo funciona, y funciona de manera estable , por lo que puedo ver. Después de la compactación, ambos spains cambian su puntero. Genial Pero, ¿cómo hacer que funcione en un proyecto heredado?


Jit intrínseco


Olvidé por netcore que el soporte para los tramos se implementa en netcore través de intrinsik . En otras palabras, netcore puede crear punteros internos incluso para un fragmento de matriz y actualizar correctamente los enlaces cuando el GC mueve. En netframework , la implementación nuget de un span es una muleta. De hecho, tenemos dos spen diferentes: uno se crea a partir de la matriz y rastrea sus enlaces, el segundo desde el puntero y no tiene idea de a qué apunta. Después de mover la matriz original, el puntero span continúa apuntando hacia donde el puntero pasó a su constructor apuntado. A modo de comparación, este es un ejemplo de implementación de span en netcore :


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

y en netframework :


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

_pinnable contiene una referencia a la matriz, si una se pasó al constructor, _byteOffset contiene un cambio (incluso el intervalo en toda la matriz tiene algún cambio distinto de cero relacionado con la forma en que la matriz se representa en la memoria, probablemente ). Si pasa el puntero void* al constructor, simplemente se convierte en un _byteOffset absoluto. Span se enclavará estrechamente al área de memoria, y todos los métodos de instancia abundan en condiciones como if(_pinnable is null) {/* */} else {/* _pinnable */} . ¿Qué hacer en tal situación?


Cómo hacerlo no vale la pena, pero todavía lo hice


Esta sección está dedicada a varias implementaciones compatibles con netframework , que permiten netframework Span<T> -> Span<U> , manteniendo todos los enlaces necesarios.
Te advierto: esta es una zona de programación anormal con posibles errores fundamentales y un comportamiento indefinido al final


Método 1: ingenuo


Como muestra el ejemplo, la conversión de punteros no dará el resultado deseado en netframework . Necesitamos el valor _pinnable . Bien, descubriremos el reflejo sacando los campos privados (muy mal y no siempre es posible), lo escribiremos en una nueva inversión, estaremos felices. Solo hay un pequeño problema: spen es una ref struct , no puede ser un argumento genérico, ni puede empaquetarse en un object . Los métodos estándar de reflexión requerirán, de una forma u otra, empujar la luz al tipo de referencia. No encontré una manera simple (incluso considerando la reflexión en campos privados).


Método 2: necesitamos profundizar


Todo ya se ha hecho antes que yo ( [1] , [2] , [3] ). Spen es una estructura, independientemente de T tres campos ocupan la misma cantidad de memoria ( en la misma arquitectura ). ¿Qué pasa si [FieldOffset(0)] ? Apenas dicho que hecho.


 [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; } 

Pero cuando inicia el programa (o más bien, cuando intenta usar un tipo), TypeLoadException una TypeLoadException : un genérico no puede ser LayoutKind.Explicit . Bien, no importa, sigamos el camino difícil:


 [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; } 

Ahora puedes hacer esto:


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

El método funciona con un solo problema: el campo _length copia tal cual, por lo que al convertir int -> byte el intervalo de bytes es 4 veces más pequeño que la matriz real.
No hay problema


 [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; } 

Ahora a través de RawView puede acceder a cada campo de extensión individual.


 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; } 

Y funciona como debería , si ignoras el uso de trucos sucios. Menos: la versión genérica del convertidor no se puede crear, debe contentarse con tipos predefinidos.


Método 3: loco


Como cualquier programador normal, me gusta automatizar las cosas. La necesidad de escribir convertidores para cualquier par de tipos no unmanaged no me agradó. ¿Qué solución se puede ofrecer? Así es, obtenga el CLR para escribir el código por usted .


¿Cómo lograr esto? Hay diferentes formas, hay artículos . En resumen, el proceso se ve así:
Cree un generador de compilación -> cree un generador de módulos -> cree un tipo -> {Campos, métodos, etc.} -> en la salida obtenemos una instancia de Type .
Para comprender exactamente cómo debería verse el tipo (es una ref struct ), utilizamos cualquier herramienta del tipo ildasm . En mi caso, era dotPeek .
Crear un generador de tipos se parece a esto:


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

Ahora los campos. Como no podemos copiar directamente Span<T> a Span<U> debido a la diferencia en las longitudes, necesitamos crear dos tipos de cada conversión


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

Aquí Raw podemos declarar con nuestras manos y reutilizar. No te olvides de IsByRefLikeAttribute . Con los campos, todo es simple:


 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); 

Eso es todo, el tipo más simple está listo. Ahora guarde en caché el módulo de ensamblaje. Los tipos personalizados se almacenan en caché, por ejemplo, en el diccionario ( T -> Generated_{nameof(T)} ). Creamos un contenedor que, de acuerdo con los dos tipos TIn y TOut genera dos tipos de ayudantes y realiza las operaciones necesarias en los tramos. Hay uno pero. Como en el caso de la reflexión, es casi imposible usarlo en tramos (o en otras ref struct ). O no encontré una solución simple . Como ser


Delegados al rescate


Los métodos de reflexión generalmente se ven así:


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

No llevan información sobre los tipos, por lo que si el boxeo (= embalaje) es aceptable para usted, no hay problemas.
En nuestro caso, @this y otherArgs deben contener una ref struct , que no pude evitar.
Sin embargo, hay una manera más simple. Imaginemos que un tipo tiene métodos getter y setter (no propiedades, sino métodos simples creados manualmente).
Por ejemplo:


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

Además del método, podemos declarar un tipo de delegado (explícitamente en el código):


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

Tenemos que hacer esto porque la acción estándar debería tener una firma de Action<Span<T>> , pero los spenes no pueden usarse como argumentos genéricos. SpanSetterDelegate , sin embargo, es un delegado absolutamente válido.
Crea los delegados necesarios. Para hacer esto, realice manipulaciones estándar:


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

Ahora spanSetter se puede usar como, por ejemplo, spanSetter(Span<T>.Empty); . En cuanto a @this 2 , esta es una instancia de nuestro tipo dinámico, creado, por supuesto, a través de Activator.CreateInstance(type) , porque la estructura tiene un constructor predeterminado sin argumentos.


Entonces, la última frontera: necesitamos generar dinámicamente métodos.


2 Puede notar que algo va mal aquí: Activator.CreateInstance() empacando una instancia de ref struct . Ver el final de la siguiente sección.


Meet Reflection.Emit


Creo que los métodos podrían generarse usando Expression , como Los cuerpos de nuestros captadores / setters triviales consisten literalmente en un par de expresiones. Elegí un enfoque diferente y más directo.


Si observa el código IL de un captador trivial, puede ver algo como ( Debug , X86 , netframework4.8 )


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

Hay toneladas de lugares para detenerse y depurar.
En la versión de lanzamiento, solo queda lo más importante:


 ldarg.0 ldfld /* - */ ret 

El argumento nulo del método de instancia es ... this . Por lo tanto, lo siguiente está escrito en IL :
1) Descargar this
2) Cargue el valor del campo
3) traerlo de vuelta


Solo eh? Reflection.Emit tiene una sobrecarga especial que toma, además del código operativo, también un parámetro descriptor de campo. Justo lo mismo que recibimos anteriormente, por ejemplo, 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); 

Para el setter, es un poco más complicado, debe cargar esto en la pila, cargar el primer argumento de la función, luego llamar a la instrucción de escritura en el campo y no devolver nada:


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

Una vez realizado este procedimiento para el campo Raw , declarando los delegados necesarios (o utilizando los estándares), obtenemos un tipo dinámico y cuatro métodos de acceso a partir de los cuales se generan los delegados genéricos correctos.


Escribimos una clase de contenedor que, utilizando dos parámetros genéricos ( TIn , TOut ), recibe instancias de Type que hacen referencia a los tipos dinámicos (en caché) correspondientes, después de lo cual crea un objeto de cada tipo y genera cuatro delegados genéricos, a saber


  1. void SetSpan(Span<TIn> span) para escribir el span de origen en la estructura
  2. Raw GetRaw() para leer el contenido de un span como una estructura Raw
  3. void SetRaw(Raw raw) para escribir la estructura Raw modificada en el segundo objeto
  4. Span<TOut> GetSpan() para devolver el span del tipo deseado con los campos correctamente establecidos y recalculados.

Curiosamente, las instancias de tipo dinámico deben crearse una vez. Al crear un delegado, se pasa una referencia a estos objetos como un parámetro @this . Aquí hay una violación de las reglas. Activator.CreateInstance devuelve el object . Aparentemente, esto se debe al hecho de que el tipo dinámico en sí no resultó ref - type.IsByRef ( type.IsByRef Like == false ), pero fue posible crear campos ref like. Aparentemente, dicha restricción está presente en el lenguaje, pero el CLR digiere. Quizás es aquí donde se dispararán las rodillas en el caso de un uso no estándar. 3


Entonces, obtenemos una instancia de tipo genérico que contiene cuatro delegados y dos referencias implícitas a instancias de clases dinámicas. Los delegados y las estructuras se pueden reutilizar al realizar las mismas castas en una fila. Para mejorar el rendimiento, volvemos a almacenar en caché (ya un convertidor de tipos) para un par (TIn, TOut) -> Generator<TIn, TOut> .


El trazo es el último: le damos tipos, 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(); } 

Conclusión


A veces, en aras del interés deportivo, puede omitir algunas de las limitaciones del idioma e implementar una funcionalidad no estándar. Por supuesto, bajo su propio riesgo y riesgo. Vale la pena señalar que el método dinámico le permite abandonar por completo los punteros y unsafe / fixed contextos unsafe / fixed , lo que puede ser una ventaja. La desventaja obvia es la necesidad de reflexión y generación de tipos.


Para aquellos que han leído hasta el final.


Resultados de referencia ingenuos

¿Y qué tan rápido es todo?
Comparé la velocidad de las castas en un escenario tonto que no refleja el uso real / potencial de tales castas y vanos, pero al menos da una idea de la velocidad.


  1. Cast_Explicitutiliza la conversión a través de un tipo declarado explícitamente, como en el Método 2 . Cada casta requiere la asignación de dos estructuras pequeñas y accesos a los campos;
  2. Cast_ILimplementa el Método 3 , pero cada vez crea una instancia nuevamente Generator<TIn, TOut>, lo que lleva a búsquedas constantes en los diccionarios, después de que el primer pase genera todos los tipos;
  3. Cast_IL_Cachedalmacena en caché la instancia del convertidor directamente Generator<TIn, TOut>, por lo que resulta ser más rápido en promedio, porque toda la casta se reduce a las llamadas de cuatro delegados;
  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 

MethodNMeanErrorStdDevMedianRatioRatioSD
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_IL1,000,0002,263,826.7 ns23,724.4930 ns22,191.9054 ns2,262,000.0 ns0.900.01
Cast_IL_Cached1,000,0002,265,186.7 ns19,505.5913 ns18,245.5422 ns2,266,300.0 ns0.900.01
Buffer1,000,0001,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 , , .


PS


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 — . Porque ,


 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/465077/


All Articles