Unsafe.AsSpan: Span como substituir ponteiros?


C# é uma linguagem incrivelmente flexível. Nele, você pode escrever não apenas os aplicativos de back-end ou de desktop. Eu uso o C# para trabalhar com dados científicos, que impõem certos requisitos às ferramentas disponíveis no idioma. Embora o netcore a agenda (considerando que, após o netstandard2.0 maioria dos recursos de idiomas e tempo de execução não é netframework para o netframework ), continuo trabalhando com projetos legados.


Neste artigo, analiso uma aplicação não óbvia (mas provavelmente desejada?) Do Span<T> e a diferença entre a implementação do Span<T> no netframework e netcore devido às especificidades do clr .


Disclaimer 1

Os trechos de código deste artigo não são de forma alguma destinados ao uso em projetos do mundo real.


A solução proposta para o problema (rebuscado?) É mais uma prova de conceito.
De qualquer forma, ao implementá-lo em seu projeto, você faz isso por seu próprio risco e risco.


Isenção 2

Estou absolutamente certo de que em algum lugar, em alguns casos, isso definitivamente matará alguém no joelho.


C# improvável que o desvio de segurança de tipo em C# leve a algo bom.


Por razões óbvias, não testei esse código em todas as situações possíveis; no entanto, os resultados preliminares parecem promissores.


Por que eu preciso do Span<T> ?


O Spen permite que você trabalhe com matrizes de tipos unmanaged uma forma mais conveniente, reduzindo o número de alocações necessárias. Apesar do suporte à extensão na estrutura de netframework BCL netframework quase completamente ausente, várias ferramentas podem ser obtidas usando System.Memory , System.Buffers e System.Runtime.CompilerServices.Unsafe .
O uso de vãos no meu projeto legado é limitado, no entanto, achei um uso não óbvio, enquanto cuspia na segurança de tipos.
O que é esta aplicação? No meu projeto, trabalho com dados obtidos de uma ferramenta científica. São imagens, que, em geral, são uma matriz de T[] , onde T é um dos tipos primitivos unmanaged , por exemplo, Int32 (também conhecido como int ). Para serializar corretamente essas imagens em disco, preciso oferecer suporte ao formato legado incrivelmente inconveniente, proposto em 1981 e que pouco mudou desde então. O principal problema desse formato é o BigEndian . Portanto, para escrever (ou ler) uma matriz não compactada de T[] , você precisa alterar a endianess de cada elemento. A tarefa trivial.
Quais são algumas soluções óbvias?


  1. Nós iteramos sobre a matriz T[] , chamamos BitConverter.GetBytes(T) , expandimos esses poucos bytes, BitConverter.GetBytes(T) para a matriz de destino.
  2. Nós iteramos sobre a matriz T[] , executamos fraudes no formato new byte[] {(byte)((x & 0xFF00) >> 8), (byte)(x & 0x00FF)}; (deve funcionar em tipos de byte duplo), escreva na matriz de destino.
  3. * Mas T[] uma matriz? Os elementos estão em uma fileira, certo? Assim, você pode percorrer todo o caminho, por exemplo, Buffer.BlockCopy(intArray, 0, byteArray, 0, intArray.Length * sizeof(int)); . O método copia a matriz para a verificação de tipo de matriz ignorada. Só é necessário não perder os limites e a alocação. Nós misturamos os bytes como resultado.
  4. * Eles dizem que C# é (C++)++ . Portanto, habilite /unsafe , fixed(int* p = &intArr[0]) byte* bPtr = (byte*)p; e agora você pode executar em torno da representação de bytes da matriz de origem, alterar endianess rapidamente e gravar blocos no disco (adicionando stackalloc byte[] ou ArrayPool<byte>.Shared pelo buffer intermediário) sem alocar memória para uma matriz de bytes totalmente nova.

Parece que o ponto 4 permite resolver todos os problemas, mas o uso explícito de contexto unsafe e o trabalho com ponteiros é de alguma forma completamente diferente. Então Span<T> vem em nosso auxílio.


Span<T>


Span<T> tecnicamente deve fornecer ferramentas para trabalhar com plotagens de memória quase como trabalhar com ponteiros, eliminando a necessidade de "consertar" a matriz na memória. Esse ponteiro de reconhecimento de GC com limites de matriz. Está tudo bem e seguro.
Uma coisa, mas - apesar da riqueza de System.Runtime.CompilerServices.Unsafe , Span<T> pregado no tipo T Dado que o spen é essencialmente um ponteiro de 1 + comprimento, e se você puxar o ponteiro, convertê-lo em outro tipo, recalcular o comprimento e fazer uma nova extensão? Felizmente, temos public Span<T>(void* pointer, int length) .
Vamos escrever um teste simples:


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

Desenvolvedores mais avançados do que eu deveria perceber imediatamente o que está errado aqui. O teste falhará? A resposta, como geralmente acontece, depende .
Nesse caso, depende principalmente do tempo de execução. No netcore teste deve funcionar, mas no netframework , como netframework .
Curiosamente, se você remover alguns dos ensaios, o teste começará a funcionar corretamente em 100% dos casos.
Vamos acertar.


1 eu estava errado .


Resposta correta: depende


Por que o resultado depende ?
Vamos remover todos os desnecessários e escrever aqui esse 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); } 

O método Write<T, U> aceita uma extensão do tipo T , lê o endereço do primeiro elemento e lê nesse ponteiro um elemento do tipo U Em outras palavras, Write<int, int>(ref x) produzirá o endereço na memória + o número 999.
Write normal imprime uma matriz.
Agora, sobre o 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# sintaxe C# agora suporta esse registro de estado fixed chamando implicitamente o método Span<T>.GetPinnableReference() .
Execute este método no netframework4.8 no modo x64 . Nós olhamos o que acontece:


 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, os dois intervalos (apesar dos diferentes tipos) se comportam de forma idêntica, e o Span<byte> , em essência, representa uma visualização em bytes da matriz original. O que você precisa
Ok, vamos tentar mudar o início do período para o tamanho de um IntPtr (ou 2 X int em x64 ) e ler. Obtemos o terceiro elemento da matriz e o endereço correto. E depois vamos recolher o lixo ...


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

O último sinalizador neste método pede ao GC compactar o heap. Depois de chamar GC.Collect GC move a matriz local original. Span<int> reflete essas alterações, mas nosso Span<byte> continua apontando para o endereço antigo, onde agora não está claro o que. Uma ótima maneira de atirar em todos os joelhos de uma só vez!


Agora, vamos ver o resultado do mesmo fragmento de código chamado 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 ] 

Tudo funciona, e funciona de forma estável , tanto quanto eu posso ver. Após a compactação, ambos os spains mudam de ponteiro. Ótimo! Mas como agora fazê-lo funcionar em um projeto herdado?


Jit intrínseco


Eu absolutamente esqueci que o suporte a extensões é implementado no netcore através do intrinsik . Em outras palavras, o netcore pode criar ponteiros internos até para um fragmento de matriz e atualizar corretamente os links quando o GC move. No netframework , a implementação de nuget de um span é uma muleta. De fato, temos dois spens diferentes: um é criado a partir da matriz e rastreia seus links, o segundo a partir do ponteiro e não faz ideia do que ele aponta. Depois de mover a matriz original, o ponteiro de amplitude continua a apontar para onde o ponteiro passou para o construtor apontado. Para comparação, este é um exemplo de implementação de span no netcore :


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

e na netframework :


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

_pinnable contém uma referência à matriz, se uma foi passada ao construtor, _byteOffset contém uma mudança (mesmo a extensão em toda a matriz tem alguma mudança diferente de zero relacionada à maneira como a matriz é representada na memória, provavelmente ). Se você passar o ponteiro void* para o construtor, ele é simplesmente convertido em um absoluto _byteOffset . A extensão será pregada firmemente na área da memória e todos os métodos de instância abundam com condições como if(_pinnable is null) {/* */} else {/* _pinnable */} . O que fazer em tal situação?


Como fazer isso não vale a pena, mas eu ainda fiz


Esta seção é dedicada a várias implementações suportadas pelo netframework , que permitem netframework Span<T> -> Span<U> , mantendo todos os links necessários.
Eu aviso: esta é uma zona de programação anormal com possíveis erros fundamentais e um comportamento indefinido no final


Método 1: Ingênuo


Como o exemplo mostrou, a conversão de ponteiros não fornecerá o resultado desejado na netframework da netframework . Precisamos do valor _pinnable . Ok, descobriremos o reflexo retirando os campos particulares (muito ruins e nem sempre possíveis), escreveremos em um novo espaço, seremos felizes. Há apenas um pequeno problema: spen é uma ref struct , não pode ser um argumento genérico, nem pode ser empacotado em um object . Os métodos padrão de reflexão exigirão, de uma maneira ou de outra, empurrar a extensão para o tipo de referência. Não encontrei uma maneira simples (mesmo considerando a reflexão em campos particulares).


Método 2: precisamos ir mais fundo


Tudo já foi feito antes de mim ( [1] , [2] , [3] ). Spen é uma estrutura, independentemente de T três campos ocupam a mesma quantidade de memória ( na mesma arquitetura ). E se [FieldOffset(0)] ? Mal disse o que fez.


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

Mas quando você inicia o programa (ou melhor, ao tentar usar um tipo), uma TypeLoadException encontra - um genérico não pode ser LayoutKind.Explicit . Ok, não importa, vamos seguir o caminho 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; } 

Agora você pode fazer isso:


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

O método funciona com apenas um problema - o campo _length copiado como está; portanto, ao _length int -> byte a extensão de bytes é 4 vezes menor que a matriz real.
Não é um 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; } 

Agora, através do RawView você pode acessar cada campo de amplitude 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; } 

E funciona como deveria , se você ignorar o uso de truques sujos. Menos - a versão genérica do conversor não pode ser criada; você precisa se contentar com tipos predefinidos.


Método 3: Louco


Como qualquer programador normal, eu gosto de automatizar as coisas. A necessidade de escrever conversores para qualquer par de tipos não unmanaged não me agradou. Que solução pode ser oferecida? É isso mesmo, faça o CLR escrever o código para você .


Como conseguir isso? Existem maneiras diferentes, existem artigos . Em suma, o processo é assim:
Crie um construtor de construção -> crie um construtor de módulo -> crie um tipo -> {Fields, Methods, etc.} -> na saída, obtemos uma instância do Type .
Para entender exatamente como deve ser o tipo (é uma ref struct ), usamos qualquer ferramenta do tipo ildasm . No meu caso, foi o dotPeek .
Criar um construtor de tipos é algo como isto:


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

Agora os campos. Como não podemos copiar diretamente Span<T> para Span<U> devido à diferença de comprimentos, precisamos criar dois tipos de elenco


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

Aqui Raw podemos declarar com nossas mãos e reutilizar. Não se esqueça de IsByRefLikeAttribute . Com campos, tudo é simples:


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

Isso é tudo, o tipo mais simples está pronto. Agora, armazene em cache o módulo de montagem. Os tipos personalizados são armazenados em cache, por exemplo, no dicionário ( T -> Generated_{nameof(T)} ). Criamos um invólucro que, de acordo com os dois tipos TIn e TOut gera dois tipos de auxiliares e executa as operações necessárias nos vãos. Existe um mas. Como no caso da reflexão, é quase impossível usá-lo em vãos (ou em outras ref struct ). Ou não encontrei uma solução simples . Como ser


Delegados para o resgate


Os métodos de reflexão geralmente se parecem com isso:


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

Eles não carregam informações sobre os tipos; portanto, se o boxe (= embalagem) for aceitável para você, não haverá problemas.
No nosso caso, @this e otherArgs devem conter uma ref struct , que eu não consegui contornar.
No entanto, existe uma maneira mais simples. Vamos imaginar que um tipo tenha métodos getter e setter (não propriedades, mas métodos simples criados manualmente).
Por exemplo:


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

Além do método, podemos declarar um tipo de delegado (explicitamente no código):


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

Temos que fazer isso porque a ação padrão teria que ter uma assinatura Action<Span<T>> , mas spenes não podem ser usados ​​como argumentos genéricos. SpanSetterDelegate , no entanto, é um representante absolutamente válido.
Crie os delegados necessários. Para fazer isso, execute manipulações padrão:


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

Agora, spanSetter pode ser usado como, por exemplo, spanSetter(Span<T>.Empty); . Quanto a @this 2 , essa é uma instância do nosso tipo dinâmico, criada, é claro, por meio de Activator.CreateInstance(type) , porque a estrutura possui um construtor padrão sem argumentos.


Portanto, a última fronteira - precisamos gerar métodos dinamicamente.


2 Você pode perceber que algo está errado aqui - Activator.CreateInstance() compactando uma instância de ref struct . Veja o final da próxima seção.


Conheça Reflection.Emit


Eu acho que os métodos podem ser gerados usando Expression , como os corpos de nossos getters / setters triviais consistem literalmente em algumas expressões. Eu escolhi uma abordagem diferente e mais direta.


Se você olhar para o código IL de um getter trivial, poderá ver algo como ( Debug , X86 , netframework4.8 )


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

Existem muitos lugares para parar e depurar.
Na versão de lançamento, apenas o mais importante permanece:


 ldarg.0 ldfld /* - */ ret 

O argumento nulo do método de instância é ... this . Assim, o seguinte está escrito em IL :
1) Faça this download this
2) Carregue o valor do campo
3) Traga de volta


Apenas hein? Reflection.Emit tem uma sobrecarga especial que leva, além do código op, também um parâmetro do descritor de campo. Da mesma forma que recebemos anteriormente, por exemplo, 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 o setter, é um pouco mais complicado, você precisa carregar isso na pilha, carregar o primeiro argumento da função, chamar a instrução write no campo e não retornar nada:


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

Depois de executar esse procedimento no campo Raw , declarando os delegados necessários (ou usando os padrão), obtemos um tipo dinâmico e quatro métodos de acesso, a partir dos quais os delegados genéricos corretos são gerados.


Escrevemos uma classe de wrapper que, usando dois parâmetros genéricos ( TIn , TOut ), recebe instâncias do tipo Type que fazem referência aos tipos dinâmicos correspondentes (em cache), após o qual cria um objeto de cada tipo e gera quatro delegados genéricos, a saber


  1. void SetSpan(Span<TIn> span) para gravar o span de origem na estrutura
  2. Raw GetRaw() para ler o conteúdo de um intervalo como uma estrutura Raw
  3. void SetRaw(Raw raw) para gravar a estrutura Raw modificada no segundo objeto
  4. Span<TOut> GetSpan() para retornar o span do tipo desejado com os campos corretamente definidos e recalculados.

Curiosamente, as instâncias de tipo dinâmico precisam ser criadas uma vez. Ao criar um delegado, uma referência a esses objetos é passada como um parâmetro @this . Aqui está uma violação das regras. Activator.CreateInstance retorna o object . Aparentemente, isso se deve ao fato de que o próprio tipo dinâmico não type.IsByRef ref like ( type.IsByRef Like == false ), mas foi possível criar campos semelhantes a ref . Aparentemente, essa restrição está presente no idioma, mas o CLR digere. Talvez seja aqui que os joelhos serão baleados no caso de uso fora do padrão. 3


Portanto, obtemos uma instância de um tipo genérico que contém quatro delegados e duas referências implícitas a instâncias de classes dinâmicas. Delegados e estruturas podem ser reutilizados ao executar as mesmas castas em uma linha. Para melhorar o desempenho, armazenamos em cache novamente (já um conversor de tipos) para um par (TIn, TOut) -> Generator<TIn, TOut> .


O golpe é o último: 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(); } 

Conclusão


Às vezes - por uma questão de interesse esportivo - você pode ignorar algumas das limitações do idioma e implementar funcionalidades não padrão. Claro, por sua conta e risco. Vale ressaltar que o método dinâmico permite abandonar completamente ponteiros e contextos unsafe / fixed , o que pode ser um bônus. A desvantagem óbvia é a necessidade de reflexão e geração de tipos.


Para quem leu até o fim.


Resultados ingênuos de referência

E quão rápido é tudo isso?
Comparei a velocidade das castas em um cenário estúpido que não reflete o uso real / potencial dessas castas e extensões, mas pelo menos dá uma idéia da velocidade.


  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 

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


All Articles