Janeiro de 2018

Volume 33 – Número 1

C# - Sobre Span: Explorando um Novo Suporte do .NET

Por Stephen Toub | Janeiro de 2018

Imagine que você está expondo uma rotina de classificação especializada para funcionar localmente nos dados na memória. É provável que você exponha um método que use uma matriz e ofereça uma implementação que funciona em T[]. Isso é ótimo se o chamador do seu método tem uma matriz e deseja classificá-la inteiramente, mas e se o chamador quiser classificar somente uma parte da matriz? Você provavelmente também expôs uma sobrecarga que usou um deslocamento e uma contagem. Mas e se você quisesse dar suporte a dados na memória que, em vez de estar em uma matriz, são provenientes de código nativo, por exemplo, ou residem na pilha e você só tem um ponteiro e o tamanho? Como você poderia codificar seu método de classificação que funcionaria em uma região de memória arbitrária, mas também funcionaria da mesma forma com matrizes inteiras ou subconjuntos de matrizes, e também com matrizes gerenciadas e ponteiros não gerenciados?

Vejamos outro exemplo. Você está implementando uma operação em System.String, por exemplo, um método de análise especializada. É provável que você exponha um método que use uma cadeia de caracteres e ofereça uma implementação que funciona em cadeias de caracteres. Mas e se você quisesse dar suporte ao funcionamento em um subconjunto dessa cadeia de caracteres? String.Substring poderia ser usada para isolar somente a parte que interessa, mas é uma operação relativamente cara que envolve alocação de uma cadeia de caracteres e cópia de memória. Você poderia, conforme mencionado no exemplo da matriz, usar um deslocamento e uma contagem, mas e se o chamador só tiver char[] em vez de uma cadeia de caracteres? E se o chamador tiver um char*, por exemplo, algum criado com stackalloc para usar espaço na pilha, ou em função de uma chamada a código nativo? Como você poderia codificar seu próprio método de análise de modo a não obrigar o chamador a fazer alocações ou cópias, mas que funcionasse bem também com entradas do tipo string, char[] e char*?

Nos dois casos, você poderia usar código não gerenciado e ponteiros, expondo uma implementação que aceitaria um ponteiro e um tamanho. No entanto, isso elimina as garantias de segurança essenciais ao .NET e permitem o surgimento de problemas como estouro de buffer e violações de acesso que, para a maioria dos desenvolvedores de .NET, não existem mais. Isso também viabiliza outros prejuízos ao desempenho, como a necessidade de fixar objetos gerenciados durante a operação, de forma que o ponteiro recuperado permaneça válido. E, dependendo do tipo de dados envolvido, pode não ser prático obter um ponteiro.

Existe uma solução para esse problema: seu nome é Span<T>.

O que é Span<T>?

System.Span<T> é um novo tipo de valor no .NET. Ele permite a representação de regiões contíguas de memória arbitrária; não importa se a memória está associada a um objeto gerenciado, foi fornecida por código nativo por interop ou está na pilha. E ele faz isso oferecendo acesso seguro com características de desempenho semelhantes às das matrizes.

Por exemplo, você pode criar um Span<T> de uma matriz:

var arr = new byte[10];
Span<byte> bytes = arr; // Implicit cast from T[] to Span<T>

Nela, você pode criar um span com facilidade e eficiência para representar/apontar somente para um subconjunto dessa matriz, usando uma sobrecarga do método Slice do span. Daí, você pode indexar o span resultante para gravar e ler dados na parte relevante da matriz original:

Span<byte> slicedBytes = bytes.Slice(start: 5, length: 2);
slicedBytes[0] = 42;
slicedBytes[1] = 43;
Assert.Equal(42, slicedBytes[0]);
Assert.Equal(43, slicedBytes[1]);
Assert.Equal(arr[5], slicedBytes[0]);
Assert.Equal(arr[6], slicedBytes[1]);
slicedBytes[2] = 44; // Throws IndexOutOfRangeException
bytes[2] = 45; // OK
Assert.Equal(arr[2], bytes[2]);
Assert.Equal(45, arr[2]);

Como mencionamos, spans são mais do que apenas uma maneira de acessar e criar subconjuntos de matrizes. Eles também podem ser usados para fazer referência a dados na pilha. Por exemplo:

Span<byte> bytes = stackalloc byte[2]; // Using C# 7.2 stackalloc support for spans
bytes[0] = 42;
bytes[1] = 43;
Assert.Equal(42, bytes[0]);
Assert.Equal(43, bytes[1]);
bytes[2] = 44; // throws IndexOutOfRangeException

De maneira geral, eles podem ser usados para fazer referência a tamanhos e ponteiros arbitrários, por exemplo, a memória alocada de um heap nativo, desta forma:

IntPtr ptr = Marshal.AllocHGlobal(1);
try
{
  Span<byte> bytes;
  unsafe { bytes = new Span<byte>((byte*)ptr, 1); }
  bytes[0] = 42;
  Assert.Equal(42, bytes[0]);
  Assert.Equal(Marshal.ReadByte(ptr), bytes[0]);
  bytes[1] = 43; // Throws IndexOutOfRangeException
}
finally { Marshal.FreeHGlobal(ptr); }

O indexador Span<T> se beneficia de um recurso da linguagem C# apresentado no C# 7.0 chamado retornos ref. O indexador é declarado com um tipo de retorno “ref T”, que fornece semântica semelhante à de indexação de duas matrizes, retornando uma referência à localização real do armazenamento em vez de retornar uma cópia do que reside no local:

public ref T this[int index] { get { ... } }

O impacto desse indexador que retorna ref fica mais óbvio com exemplos, como a comparação com o indexador List<T>, que não retorna ref. Veja um exemplo:

struct MutableStruct { public int Value; }
...
Span<MutableStruct> spanOfStructs = new MutableStruct[1];
spanOfStructs[0].Value = 42;
Assert.Equal(42, spanOfStructs[0].Value);
var listOfStructs = new List<MutableStruct> { new MutableStruct() };
listOfStructs[0].Value = 42; // Error CS1612: the return value is not a variable

Uma segunda variante de Span<T>, chamada System.ReadOnlySpan<T>, permite o acesso somente leitura. Esse tipo é semelhante a Span<T>; no entanto, ele se beneficia de um novo recurso do C# 7.2 para retornar “ref readonly T” em vez de “ref T,” permitindo que ele funcione com tipos de dados imutáveis, como System.String. ReadOnlySpan<T> agiliza a criação de fatias de cadeias de caracteres sem alocação ou cópia, conforme mostrado abaixo:

string str = "hello, world";
string worldString = str.Substring(startIndex: 7, length: 5); // Allocates ReadOnlySpan<char> worldSpan =
  str.AsReadOnlySpan().Slice(start: 7, length: 5); // No allocation
Assert.Equal('w', worldSpan[0]);
worldSpan[0] = 'a'; // Error CS0200: indexer cannot be assigned to

Spans oferecem vários benefícios que vão além dos que já foram mencionados. Por exemplo, spans dão suporte à noção de reinterpretação de conversões, o que significa que você pode converter um Span<byte> em um Span<int> (em que o índice 0 do Span<int> mapeia para os primeiros quatro bytes do Span<byte>). Assim, se você ler um buffer ou bytes, poderá transmiti-los a métodos que operam em bytes agrupados como ints com segurança e eficiência.

Como é a implementação do Span<T>?

Os desenvolvedores normalmente não precisam entender como uma biblioteca usada é implementada. No entanto, no caso do Span<T>, vale a pena conhecer minimamente os detalhes por trás dele, já que esses detalhes têm relação tanto com o desempenho quanto com as restrições de uso.

Primeiro, Span<T> é um tipo de valor que contém uma ref e um tamanho, definido mais ou menos assim:

public readonly ref struct Span<T>
{
  private readonly ref T _pointer;
  private readonly int _length;
  ...
}

O conceito de um campo ref T pode parecer estranho inicialmente: na verdade, ninguém pode realmente declarar um campo ref T em C# ou mesmo em MSIL. Mas o Span<T> é, na verdade, codificado para usar um tipo interno especial no tempo de execução que é tratado como intrínseco JIT (just-in-time), em que o JIT gera para ele o equivalente a um campo ref T. Pense em um uso de ref que é muito mais conhecido:

public static void AddOne(ref int value) => value += 1;
...
var values = new int[] { 42, 84, 126 };
AddOne(ref values[2]);
Assert.Equal(127, values[2]);

Esse código transmite um slot na matriz por referência, para que (tirando as otimizações) você tenha uma ref T na pilha. A ref T no Span<T> usa a mesma ideia, apenas encapsulado em um struct. Tipos que contêm essas refs direta ou indiretamente são chamados de tipos semelhantes a ref, e o compilador C# 7.2 permite a declaração desses tipos semelhantes a ref usando ref struct na assinatura.

Com essa breve descrição, duas coisas devem estar claras:

  1. Span<T> é definido de forma que as operações possam ser tão eficientes quanto são nas matrizes: a indexação em um span não exige computação para determinar o início de um ponteiro e seu deslocamento inicial, já que o próprio campo ref já encapsula ambos. (Ao contrário, ArraySegment<T> tem um campo de deslocamento separado, encarecendo a indexação e a transmissão.)
  2. A natureza do Span<T> como tipo semelhante a ref traz algumas limitações devido ao campo ref T.

Esse segundo item tem algumas consequências interessantes que levam o .NET a conter um segundo conjunto de tipos associado, liderado por Memory<T>.

O que é Memory<T> e por que você precisaria dele?

Span<T> é um tipo semelhante a ref porque contém um campo ref, e os campos ref podem fazer referência não só ao início de objetos como matrizes, mas também ao meio delas:

var arr = new byte[100];
Span<byte> interiorRef1 = arr.AsSpan().Slice(start: 20);
Span<byte> interiorRef2 = new Span<byte>(arr, 20, arr.Length – 20);
Span<byte> interiorRef3 =
  Span<byte>.DangerousCreate(arr, ref arr[20], arr.Length – 20);

Essas referências se chamam ponteiros interiores, e acompanhá-los é uma operação relativamente cara para o coletor de lixo do tempo de execução do .NET. Assim, o tempo de execução restringe essas refs para residir somente na pilha, já que ela fornece um limite baixo implícito à quantidade de ponteiros internos que possam existir.

Além disso, o Span<T> mostrado anteriormente é maior do que o tamanho de palavras do computador, o que significa que ler e gravar em span não é uma operação atômica. Se vários threads leem e gravam campos do span no heap ao mesmo tempo, existe um risco de “dilaceramento”. Imagine um span já inicializado contendo uma referência válida e um _length correspondente de 50. Um thread começa a gravar um novo span nele e consegue gravar até o novo valor _pointer. Em seguida, antes de poder definir o _length como 20, um segundo thread lê o span, inclusive o novo _pointer, mas com o _length antigo (e mais longo).

Com isso, as instâncias do Span<T> só podem residir na pilha, não no heap. Isso significa que você não pode fazer boxing em spans (e, consequentemente, não pode usar Span<T> com APIs de invocação de reflexão, por exemplo, já que elas exigem conversão boxing). Isso significa que você não pode ter campos Span<T> em classes, ou mesmo em structs que não sejam semelhantes a ref. Isso significa que você não pode usar spans em locais onde eles possam se tornar campos em classes implicitamente, por exemplo, capturando-os em lambdas ou localmente em iteradores ou métodos assíncronos (já que esses “locais” podem acabar sendo campos nos computadores de estado gerado por compilador). Isso significa que você não pode usar Span<T> como argumento genérico, já que instâncias desse argumento de tipo poderiam entrar em boxing ou ser armazenadas no heap (e, atualmente, não existe restrição “where T : ref struct” disponível).

Essas limitações são imateriais para vários cenários, especialmente para funções de processamento síncronas e associadas ao cálculo. Mas a funcionalidade assíncrona é outra história. A maioria dos problemas citados no início do artigo em relação a matrizes, fatias de matriz, memória nativa e afins existem tanto em operações síncronas quanto assíncronas. Mesmo assim, se Span<T> não puder ser armazenado no heap e persistido pelas operações assíncronas, qual é a solução? Memory<T>.

Memory<T> looks very much like an ArraySegment<T>:
public readonly struct Memory<T>
{
  private readonly object _object;
  private readonly int _index;
  private readonly int _length;
  ...
}

Você pode criar um Memory<T> de uma matriz e fatiá-lo como faria com um span, mas ele é um struct (não semelhante a ref) e pode residir no heap. Em seguida, quando você quiser fazer um processamento síncrono, poderá obter um Span<T> dele, por exemplo:

static async Task<int> ChecksumReadAsync(Memory<byte> buffer, Stream stream)
{
  int bytesRead = await stream.ReadAsync(buffer);
  return Checksum(buffer.Span.Slice(0, bytesRead));
  // Or buffer.Slice(0, bytesRead).Span
}
static int Checksum(Span<byte> buffer) { ... }

Da mesma forma que Span<T> e ReadOnlySpan<T>, Memory<T> tem um equivalente somente leitura, ReadOnlyMemory<T>. E, como esperado, a propriedade Span retorna um ReadOnlySpan<T>. Confira a Figura 1 para ver um resumo de mecanismos internos de conversão entre esses tipos.

Figura 1 Conversões sem alocação/sem cópia entre tipos relativos a span

De Para Mecanismo
ArraySegment<T> Memory<T> Conversão implícita, método AsMemory
ArraySegment<T> ReadOnlyMemory<T> Conversão implícita, método AsReadOnlyMemory
ArraySegment<T> ReadOnlySpan<T> Conversão implícita, método AsReadOnlySpan
ArraySegment<T> Span<T> Conversão implícita, método AsSpan
ArraySegment<T> T[] Propriedade Array
Memory<T> ArraySegment<T> Método TryGetArray
Memory<T> ReadOnlyMemory<T> Conversão implícita, método AsReadOnlyMemory
Memory<T> Span<T> Propriedade Span
ReadOnlyMemory<T> ArraySegment<T> Método DangerousTryGetArray
ReadOnlyMemory<T> ReadOnlySpan<T> Propriedade Span
ReadOnlySpan<T> ref readonly T Acessador get de indexador, métodos de marshaling
Span<T> ReadOnlySpan<T> Conversão implícita, método AsReadOnlySpan
Span<T> ref T Acessador get de indexador, métodos de marshaling
String ReadOnlyMemory<char> Método AsReadOnlyMemory
String ReadOnlySpan<char> Conversão implícita, método AsReadOnlySpan
T[] ArraySegment<T> Ctor, conversão implícita
T[] Memory<T> Ctor, conversão implícita, método AsMemory
T[] ReadOnlyMemory<T> Ctor, conversão implícita, método AsReadOnlyMemory
T[] ReadOnlySpan<T> Ctor, conversão implícita, método AsReadOnlySpan
T[] Span<T> Ctor, conversão implícita, método AsSpan
void* ReadOnlySpan<T> Ctor
void* Span<T> Ctor

Você verá que o campo _object de Memory<T> não está tipificado claramente como T[]; na verdade, ele é armazenado como um objeto. Isso mostra que Memory<T> pode encapsular outras coisas além de matrizes, como System.Buffers.OwnedMemory<T>. OwnedMemory<T> é uma classe abstrata que pode ser usada para encapsular dados que precisam ter seu ciclo de vida gerenciado detalhadamente, como a memória recuperada de um pool. Esse é um tópico avançado fora do escopo deste artigo, mas é como Memory<T> pode ser usado para encapsular ponteiros na memória nativa, por exemplo. ReadOnlyMemory<char> também pode ser usado com cadeias de caracteres, assim como ReadOnlySpan<char>.

Como Span<T> e Memory<T> se integram a bibliotecas do .NET?

No trecho de código de Memory<T> anterior, você verá uma chamada para Stream.ReadAsync que está sendo transmitida em um Memory<byte>. Mas Stream.ReadAsync no .NET atualmente está definido para aceitar um byte[]. Como isso funciona?

Para dar suporte a Span<T> e afins, centenas de novos membros e tipos estão sendo adicionados ao .NET. Muitos deles são sobrecargas de métodos baseados em matrizes ou em cadeias de caracteres existentes; já outros são tipos totalmente novos voltados para áreas de processamento específicas. Por exemplo, todos os tipos primitivos como Int32 têm agora sobrecargas Parse que aceitam ReadOnlySpan<char> além das sobrecargas existentes que usam cadeias de caracteres. Imagine que você esteja esperando uma cadeia de caracteres contendo dois números separados por uma vírgula (como “123,456”) e deseja analisar esses dois números. Atualmente, você deve codificar mais ou menos assim:

string input = ...;
int commaPos = input.IndexOf(',');
int first = int.Parse(input.Substring(0, commaPos));
int second = int.Parse(input.Substring(commaPos + 1));

Isso, no entanto, gera duas alocações de cadeias de caracteres. Se você está codificando para desempenho, isso pode ser alocações em excesso. Em vez disso, codifique assim:

string input = ...;
ReadOnlySpan<char> inputSpan = input.AsReadOnlySpan();
int commaPos = input.IndexOf(',');
int first = int.Parse(inputSpan.Slice(0, commaPos));
int second = int.Parse(inputSpan.Slice(commaPos + 1));

Usando as novas sobrecargas Parse baseadas em span, você evita a necessidade de alocações nessa operação. Existem outros métodos de análise e formatação para primitivos, como Int32, até tipos essenciais, como DateTime, TimeSpan e Guid, e até para tipos de níveis mais altos, como BigInteger e IPAddress.

Na verdade, vários métodos foram adicionados ao longo da estrutura. De System.Random a System.Text.StringBuilder a System.Net.Sockets, sobrecargas foram adicionadas para fazer com que o trabalho envolvendo {ReadOnly}Span<T> e {ReadOnly}Memory<T> seja simples e eficiente. Alguns deles até trazem benefícios adicionais. Por exemplo, Stream agora tem este método:

public virtual ValueTask<int> ReadAsync(
  Memory<byte> destination,
  CancellationToken cancellationToken = default) { ... }

Você verá que, diferentemente do método ReadAsync existente que aceita um byte[] e retorna um Task<int>, essa sobrecarga não só aceita Memory<byte> em vez de byte[], mas também retorna ValueTask<int> em vez de Task<int>. ValueTask<T> é um struct que ajuda a evitar alocações quando um método assíncrono deve com frequência retornar sincronamente, e quando não é provável que possamos armazenar uma tarefa concluída em cache para todos os valores de retorno comuns. Por exemplo, o tempo de execução pode armazenar em cache um Task<bool> concluído para um resultado verdadeiro e um para um resultado falso, mas não pode armazenar em cache quatro bilhões de objetos de tarefa para todos os valores de resultado possíveis de um Task<int>.

Como é muito frequente que as implementações de Stream armazenem em buffer para concluir as chamadas ReadAsync sincronamente, essa nova sobrecarga ReadAsync retorna um ValueTask<int>. Isso significa que as operações de leitura do Stream assíncronas que são concluídas sincronamente podem ficar totalmente livres de alocação. ValueTask<T> também é usado em outras novas sobrecargas, como as sobrecargas Socket.ReceiveAsync, Socket.SendAsync, WebSocket.ReceiveAsync e TextReader.ReadAsync.

Além disso, existem lugares em que Span<T> permite que a estrutura inclua métodos que costumavam causar problemas de segurança da memória antigamente. Imagine que você deseja criar uma cadeia de caracteres contendo um valor gerado aleatoriamente, por exemplo, para um tipo de ID. Atualmente, você pode escrever código que exija a alocação de uma matriz char, como esta:

int length = ...;
Random rand = ...;
var chars = new char[length];
for (int i = 0; i < chars.Length; i++)
{
  chars[i] = (char)(rand.Next(0, 10) + '0');
}
string id = new string(chars);

Em vez disso, você pode usar alocação de pilhas e até aproveitar Span<char> para evitar ter que usar código não gerenciado. Essa abordagem também se beneficia do novo construtor de cadeia de caracteres que aceita um ReadOnlySpan<char>, como este:

int length = ...;
Random rand = ...;
Span<char> chars = stackalloc char[length];
for (int i = 0; i < chars.Length; i++)
{
  chars[i] = (char)(rand.Next(0, 10) + '0');
}
string id = new string(chars);

Isso é melhor porque você evita a alocação de heap, mas ainda é forçado a copiar na cadeia de caracteres os dados gerados na pilha. Essa abordagem também só funciona quando a quantidade de espaço exigida é pequena o suficiente para a pilha. Se o comprimento é curto, por exemplo, 32 bytes, tudo bem, mas se forem milhares de bytes, poderá ocorrer um excedente de pilha. E se você pudesse, em vez disso, gravar na memória da cadeia de caracteres diretamente? O Span<T> permite isso. Além do novo construtor da cadeia de caracteres, ela também tem um método Create:

public static string Create<TState>(
  int length, TState state, SpanAction<char, TState> action);
...
public delegate void SpanAction<T, in TArg>(Span<T> span, TArg arg);

Esse método é implementado para alocar a cadeia de caracteres e resultar em um span gravável que pode receber o conteúdo da cadeia de caracteres durante a construção. Observe que a natureza somente pilha de Span<T> é vantajosa nesse caso, fazendo com que o span (que se refere ao armazenamento interno da cadeia de caracteres) deixe de existir antes da conclusão do construtor da cadeia de caracteres, impossibilitando o uso do span para mudar a cadeia de caracteres antes que a construção seja concluída:

int length = ...;
Random rand = ...;
string id = string.Create(length, rand, (Span<char> chars, Random r) =>
{
  for (int i = 0; chars.Length; i++)
  {
    chars[i] = (char)(r.Next(0, 10) + '0');
  }
});

Agora, não só você evitou a alocação, como está gravando diretamente na memória da cadeia de caracteres no heap, o que significa que você também está evitando a cópia e não está restringido pelas limitações de tamanho da pilha.

Além dos tipos de estrutura principais estarem ganhando novos membros, vários novos tipos do .NET estão sendo desenvolvidos para trabalhar com spans, resultando em processamento eficiente em vários cenários. Por exemplo, desenvolvedores que procuram codificar microsserviços de alto desempenho e sites com muito processamento de texto poderão ter um bom ganho de desempenho se não precisarem codificar e descodificar de cadeias de caracteres ao trabalhar com UTF-8. Para isso, novos tipos como System.Buffers.Text.Base64, System.Buffers.Text.Utf8Parser e System.Buffers.Text.Utf8Formatter estão sendo adicionados. Eles funcionam em spans de bytes, que não só evitam a codificação e decodificação de Unicode, mas também permitem o funcionamento com buffers nativos comuns nos níveis mais baixos de várias pilhas de rede:

ReadOnlySpan<byte> utf8Text = ...;
if (!Utf8Parser.TryParse(utf8Text, out Guid value,
  out int bytesConsumed, standardFormat = 'P'))
  throw new InvalidDataException();

Toda essa funcionalidade não é apenas para consumo público; na verdade, a própria estrutura é capaz de usar esses novos métodos baseados em Span<T> e Memory<T> para um melhor desempenho. Sites de chamadas no .NET Core mudaram para as novas sobrecargas ReadAsync a fim de evitar alocações desnecessárias. A análise feita pela alocação de subcadeias de caracteres agora se beneficiam de análise sem alocação. Mesmo tipos de nicho, como Rfc2898DeriveBytes, estão envolvidos, beneficiando-se do novo método TryComputeHash baseado em Span<byte> em System.Security.Cryptography.Hash­Algorithm para conseguir economizar bastante em alocação (uma matriz de bytes por iteração do algoritmo, que pode ser iterado milhares de vezes) e na melhora da produtividade.

Não é apenas para as bibliotecas principais do .NET; isso vai até a pilha. O ASP.NET Core agora depende bastante de spans, por exemplo, com o analisador de HTTP do servidor Kestrel codificado neles. Futuramente, é provável que os spans sejam expostos de APIs públicas nos níveis mais baixos do ASP.NET Core, como no pipeline de middleware.

E o tempo de execução do .NET?

Uma das formas de oferecer segurança do tempo de execução do .NET é garantindo que a indexação em uma matriz não permita ultrapassar o tamanho da matriz, prática conhecida como verificação de limites. Considere este método, por exemplo:

[MethodImpl(MethodImplOptions.NoInlining)]
static int Return4th(int[] data) => data[3];

No computador x64 em que estou digitando este artigo, o assembly gerado para esse método fica assim:

sub      rsp, 40
       cmp      dword ptr [rcx+8], 3
       jbe      SHORT G_M22714_IG04
       mov      eax, dword ptr [rcx+28]
       add      rsp, 40
       ret
G_M22714_IG04:
       call     CORINFO_HELP_RNGCHKFAIL
       int3

A instrução cmp está comparando o tamanho da matriz de dados com o índice 3 e a instrução jbe subsequente passa à rotina de falha de verificação de intervalo se 3 não estiver no intervalo (para que uma exceção seja lançada). O JIT precisa gerar código que faça com que esses acessos não ultrapassem os limites da matriz, mas isso não significa que cada acesso à matriz precise de uma verificação de limites. Considere esse método Sum:

static int Sum(int[] data)
{
  int sum = 0;
  for (int i = 0; i < data.Length; i++) sum += data[i];
  return sum;
}

O JIT precisa gerar código que faça com que esses acessos aos dados[i] não ultrapassem os limites da matriz, mas como o JIT pode perceber, pela estrutura do loop, que i sempre estará no intervalo (o loop é iterado por cada elemento do início ao fim), o JIT pode otimizar as verificações de limites na matriz. Dessa forma, o código do assembly gerado para o loop fica assim:

G_M33811_IG03:
       movsxd   r9, edx
       add      eax, dword ptr [rcx+4*r9+16]
       inc      edx
       cmp      r8d, edx
       jg       SHORT G_M33811_IG03

A instrução cmp ainda está no loop, mas apenas para comparar o valor de i (conforme armazenado no registro edx) com o tamanho da matriz (conforme armazenado no registro r8d); não é necessário fazer outras verificações de limites.

O tempo de execução usa otimizações semelhantes no span (tanto Span<T> quanto ReadOnlySpan<T>). Compare o exemplo anterior com o código abaixo, em que a única mudança está no tipo de parâmetro:

static int Sum(Span<int> data)
{
  int sum = 0;
  for (int i = 0; i < data.Length; i++) sum += data[i];
  return sum;
}

O assembly gerado para esse código é quase idêntico:

G_M33812_IG03:
       movsxd   r9, r8d
       add      ecx, dword ptr [rax+4*r9]
       inc      r8d
       cmp      r8d, edx
       jl       SHORT G_M33812_IG03

O código do assembly é muito semelhante em parte por causa da eliminação das verificações de limites. Mas também devemos levar em conta o reconhecimento, pelo JIT, do indexador de span como um intrínseco, o que significa que o JIT gera código especial para o indexador em vez de converter seu código IL em assembly.

Tudo isso serve para demonstrar que o tempo de execução pode aplicar nos spans os mesmos tipos de otimização usados para matrizes, o que torna o span um mecanismo eficiente para acessar dados. Veja mais detalhes na postagem do blog bit.ly/2zywvyI.

E a linguagem e o compilador C#?

Já falei sobre recursos adicionados à linguagem e ao compilador C# para ajudar a tornar Span<T> uma ferramenta de primeira linha no .NET. Vários recursos de C# 7.2 dizem respeito a spans (e, na verdade, o compilador C# 7.2 exigirá o uso de Span<T>). Vamos ver três desses recursos.

Ref structs. Como falado anteriormente, Span<T> é um tipo semelhante a ref, que é exposto em C# desde a versão 7.2 como struct ref. Ao colocar a palavra-chave ref antes de struct, você manda o compilador C# permitir o uso de outros tipos de ref struct, por exemplo, Span<T>, como campos, e também ficar sujeito às restrições associadas ao seu tipo. Por exemplo, se você quisesse codificar um struct Enumerador para um Span<T>, o Enumerador precisaria armazenar o Span<T> e, com isso, precisaria ser ele próprio um ref struct, assim:

public ref struct Enumerator
{
  private readonly Span<char> _span;
  private int _index;
  ...
}

Inicialização stackalloc de spans. Em versões de C# anteriores, o resultado de stackalloc só podia ser armazenado em uma variável local de ponteiro. A partir de C# 7.2, stackalloc pode ser usado como parte de uma expressão e em relação a um span, e isso pode ser feito sem usar a palavra-chave não gerenciada. Assim, em vez de escrever:

Span<byte> bytes;
unsafe
{
  byte* tmp = stackalloc byte[length];
  bytes = new Span<byte>(tmp, length);
}

Você pode simplesmente escrever:

Span<byte> bytes = stackalloc byte[length];

Isso também é muito útil quando você precisa de algum espaço de rascunho para fazer uma operação, mas deseja evitar a alocação de memória de heap para tamanhos relativamente pequenos. Antigamente, você tinha duas opções:

  • Escrever dois caminhos de código totalmente diferentes, alocando e operando em memória baseada em pilha e em memória baseada em heap.
  • Fixar a memória associada à alocação gerenciada e delegar para uma implementação também usada na memória baseada em pilha e codificada com manipulação de ponteiros em código não gerenciado.

Agora, é possível alcançar o mesmo resultado sem a duplicação do código, com código gerenciado e o mínimo de pompa:

Span<byte> bytes = length <= 128 ? stackalloc byte[length] : new byte[length];
... // Code that operates on the Span<byte>

Validação do uso de span. Como os spans podem fazer referência a dados possivelmente associados a um registro de ativação, pode ser arriscado transmitir spans que podem fazer referência a memórias que já não são válidas. Por exemplo, imagine um método que tentou fazer o seguinte:

static Span<char> FormatGuid(Guid guid)
{
  Span<char> chars = stackalloc char[100];
  bool formatted = guid.TryFormat(chars, out int charsWritten, "d");
  Debug.Assert(formatted);
  return chars.Slice(0, charsWritten); // Uh oh
}

Aqui, o espaço foi alocado da pilha e está tentando retornar uma referência para ele, mas, no momento do retorno, o espaço já não é válido para uso. Ainda bem que o compilador C# detecta esse uso inválido com ref structs e causa a falha da compilação com um erro:

erro CS8352: não é possível usar ‘chars’ locais neste contexto pelo risco de exposição de variáveis referenciadas fora do escopo de declaração

O que vem a seguir?

Os tipos, métodos, otimizações de tempo de execução e outros elementos debatidos aqui estão prestes a serem incluídos no .NET Core 2.1. Depois disso, espero que sejam incluídos no .NET Framework. Os tipos principais, como Span<T>, e os novos tipos, como Utf8Parser, também devem ser disponibilizados em um pacote System.Memory.dll compatível com .NET Standard 1.1. Isso vai disponibilizar essas funções para versões existentes do .NET Framework e do .NET Core, ainda que sem algumas das otimizações implementadas quando são internas da plataforma. Uma versão prévia desse pacote já está disponível para testes; basta adicionar uma referência ao pacote System.Memory.dll do NuGet.

Lembre-se, é claro, de que existirão alterações relevantes entre a versão prévia atual e o que vier na versão estável. Algumas alterações virão de comentários dos desenvolvedores, como você, depois de experimentarem o conjunto de recursos. Por isso, experimente usá-la e fique de olho nos repositórios github.com/dotnet/coreclr e github.com/dotnet/corefx para ver o trabalho em andamento. Você também pode encontrar a documentação em aka.ms/ref72.

No fim das contas, o sucesso desse conjunto de recursos depende dos testes dos desenvolvedores, de seus comentários e da criação de suas próprias bibliotecas utilizando esses tipos, tudo para oferecer acesso seguro e eficiente à memória em programas modernos do .NET. Estamos ansiosos em ouvir a respeito das suas experiências e, melhor ainda, em trabalhar com você no GitHub para melhorar o .NET ainda mais.


Stephen Toub trabalha com .NET na Microsoft. Você pode encontrá-lo no GitHub em github.com/stephentoub.

Agradecemos aos seguintes especialistas técnicos pela revisão deste artigo: Krzysztof Cwalina, Eric Erhardt, Ahson Khan, Jan Kotas, Jared Parsons, Marek Safar, Vladimir Sadov, Joseph Tremoulet, Bill Wagner, Jan Vorlicek, Karel Zikmund


Discuta esse artigo no fórum do MSDN Magazine