Spanned é uma biblioteca .NET de alta alocação zero, que introduz alternativas compatíveis com o Span aos tipos populares de BCL e fornece soluções vetorizadas para operações comuns em vãos.
Para começar, primeiro adicione o pacote abrangente ao seu projeto. Você pode fazer isso executando o seguinte comando:
dotnet add package SpannedComo alternativa, você pode instalá -lo através do console do gerenciador de pacotes com este comando:
Install-Package SpannedObserve que o .NET acumulou inúmeras rotas de otimização ao longo do tempo, tanto dependentes da estrutura quanto de tempo de execução. Consequentemente, tornou -se extremamente desafiador para o código altamente otimizado do vizinho para diferentes estruturas em uma única base de código. Portanto, tomei uma decisão difícil de apoiar apenas uma estrutura por versão desta biblioteca. Você pode encontrar uma lista de versões da biblioteca e as versões da estrutura suportada correspondentes na tabela abaixo:
| .NET Standard 2.0 | .NET Padrão 2.1 | .NET 8+ | |
|---|---|---|---|
Embora possa ser tentador usar v0.0.1 para todas as suas necessidades, dado seu suporte para o .NET Standard 2.0 e a adoção generalizada do .NET Standard 2.0 hoje em dia, recomendo fortemente que isso Praticamente não há otimizações que se possa executar usando essa estrutura herdada. Mesmo nosso amado Span , acessível no .NET Standard 2.0 por meio do pacote System.Memory Nuget, é conhecido como "Slow Span", porque esse Span não passa de um ArraySegment reinventado, sem suporte adequado no lado do tempo de execução/JIT. Portanto, escolha a melhor versão do pacote para o seu ambiente, não a que parece se encaixar em todos eles.
Antes de entrarmos nos detalhes, vamos discutir alguns casos de borda comuns que você pode encontrar ao usar esta biblioteca e responder à pergunta: "Por que X não faz parte do .NET?" Em suma, tudo o que você pode encontrar aqui é fácil de usar e fácil de usar.
Vamos começar com um ponto óbvio. Esta biblioteca foi projetada especificamente para cenários em que você luta com unhas de dente e de dente para todos os bytes alocados e todos os nanossegundos do tempo de execução em caminhos altamente críticos. Não é para ser uma solução única para toda a sua base de código. Incorporar sem pensar nesta biblioteca em todos os cenários concebíveis pode degradar o desempenho geral do seu aplicativo, em vez de aprimorá -lo.
Lembre-se, não mergulhe de cabeça primeiro no oceano de nano otimização até ter certeza de que é necessário.
Um erro comum a evitar é passar qualquer um dos tipos fornecidos por esta biblioteca por valor (sim, isso por si só deve ajudá -lo a entender por que algo assim não deve e nunca fará parte do BCL) . Por exemplo, embora o código a seguir possa parecer bom, é realmente desastroso:
ValueStringBuilder sb = new ValueStringBuilder ( 16 ) ;
sb . AppendUserName ( user ) ;
// ...
public static void AppendUserName ( this ValueStringBuilder sb , User user )
{
sb . Append ( user . FirstName ) ;
sb . Append ( ' ' ) ;
sb . Append ( user . LastName ) ;
} Anexar a um construtor de cordas pode ampliar seu buffer interno. No entanto, desde que passamos pelo nosso ValueStringBuilder por valor (ou seja, copiou -o) , o Builder original ValueStringBuilder não estará ciente disso e continuará a usar um buffer já descartado.
Embora essa abordagem pareça funcionar com entradas higienizadas durante os testes, ela ocasionalmente falha, quebrando não apenas o seu código, mas também algumas partes aleatórias do tempo de execução do seu aplicativo, interferindo em um buffer que uma cópia do ValueStringBuilder já voltou ao pool, para que possa ser reutilizada por outra coisa.
Você pode tentar ser inteligente sobre isso e resolver o problema reescrevendo o método de extensão problemática da seguinte maneira:
public static void AppendUserName ( this in ValueStringBuilder sb , User user )
{
sb . Append ( user . FirstName ) ;
sb . Append ( ' ' ) ;
sb . Append ( user . LastName ) ;
} Agora, ValueStringBuilder é aprovado por referência, então não deve haver problemas, certo? Bem, não. O modificador in existe para reduzir os custos de copiar a totalidade de uma instância do tipo de valor passando sua referência a um método, preservando a semântica como se fosse passada pelo valor. Isso significa que quaisquer modificações estaduais do ValueStringBuilder fornecidas não serão propagadas para a instância original. Então, ainda temos o mesmo problema em nossas mãos. A única maneira correta de implementar um método que pode modificar o estado interno de uma instância do tipo de valor é realmente passando por referência:
ValueStringBuilder sb = new ValueStringBuilder ( 16 ) ;
AppendUserName ( ref sb , user ) ;
// ...
public static void AppendUserName ( ref ValueStringBuilder sb , User user )
{
sb . Append ( user . FirstName ) ;
sb . Append ( ' ' ) ;
sb . Append ( user . LastName ) ;
}Embora não seja tão chique quanto alguns gostariam, essa solução tem o benefício de realmente funcionar.
A maioria dos tipos fornecidos por esta biblioteca define um método Dispose() , permitindo seu uso com a palavra -chave using , como pode ser visto abaixo:
using ValueStringBuilder sb = new ValueStringBuilder ( stackalloc char [ 16 ] ) ;
Foo ( ref sb ) ;
return sb . ToString ( ) ; No entanto, isso não significa que eles devem ser usados com a palavra -chave using . É muito importante lembrar como o código acima é realmente reduzido:
ValueStringBuilder sb = new ValueStringBuilder ( stackalloc char [ 16 ] ) ;
try
{
Foo ( ref sb ) ;
return sb . ToString ( ) ;
}
finally
{
sb . Dispose ( ) ;
} Criar e gerenciar regiões protegidas não é gratuito. Considerando nosso foco em nano-otimizações, o impacto aqui é perceptível. Portanto, é preferível chamar manualmente Dispose() :
ValueStringBuilder sb = new ValueStringBuilder ( stackalloc char [ 16 ] ) ;
Foo ( ref sb ) ;
string result = sb . ToString ( ) ;
sb . Dispose ( ) ;
return result ;Como alternativa, verifique se o último método que você chama em um determinado tipo tem uma sobrecarga que executa a limpeza automaticamente:
ValueStringBuilder sb = new ValueStringBuilder ( stackalloc char [ 16 ] ) ;
Foo ( ref sb ) ;
return sb . ToString ( dispose : true ) ;No .NET moderno, é comum encontrar o seguinte padrão:
Ou, expressando o mesmo conceito em código:
const int StackAllocByteLimit = 1024 ;
T [ ] ? spanSource ;
scoped Span < T > span ;
if ( sizeof ( T ) * length > StackAllocByteLimit )
{
spanSource = ArrayPool < T > . Shared . Rent ( length ) ;
span = spanSource . AsSpan ( 0 , length ) ;
}
else
{
spanSource = null ;
span = stackalloc T [ length ] ;
}
DoSomeWorkWithSpan ( span ) ;
if ( spanSource is not null )
{
ArrayPool < T > . Shared . Return ( spanSource ) ;
} Não é a peça mais bonita de caldeira, é? A lógica real geralmente acaba enterrada por ele, que está longe de ser ideal. Este é o problema exato que SpanOwner pretende resolver. Aqui está a mesma lógica, mas toda a caldeira foi escondida atrás do SpanOwner :
SpanOwner < T > owner = SpanOwner < T > . ShouldRent ( length ) ? SpanOwner < T > . Rent ( length ) : stackalloc T [ length ] ;
Span < T > span = owner . Span ;
DoSomeWorkWithSpan ( span ) ;
owner . Dispose ( ) ; Muito mais fácil de escrever, muito mais fácil de ler e, o mais importante, essa abordagem fornece exatamente o mesmo desempenho, porque SpanOwner foi projetado para ser totalmente inline. Pode ser completamente eliminado do seu código por JIT:
| Método | Significar | Stddev | Razão | Tamanho do código |
|---|---|---|---|---|
| Sem_spanowner_int32 | 5.134 ns | 0,0425 ns | 1,00 | 315 b |
| With_spanowner_int32 | 4.908 ns | 0,0168 ns | 0,96 | 310 b |
ValueStringBuilder é uma reimplementação do StringBuilder , projetado para oferecer suporte a buffers alocados em empilhamento. É capaz de utilizar um pool de matriz compartilhado para expandir seu buffer interno quando necessário. ValueStringBuilder é perfeito para construir cordas compactas que podem caber na pilha; No entanto, não deve ser usado para qualquer outra coisa, porque as operações em seqüências maiores podem e degradarão o desempenho geral do seu aplicativo. O verdadeiro brilho do ValueStringBuilder emerge quando você precisa criar uma sequência de caracteres curta que não precisa ser materializada como uma corda.
ValueStringBuilder reflete todos os recursos do StringBuilder e passa com sucesso o mesmo conjunto de testes de unidade, permitindo que ele sirva perfeitamente como substituição na maioria dos cenários.
// Note that providing a capacity instead of a buffer will force
// the builder to rent an array from `ArrayPool<char>.Shared`.
ValueStringBuilder sb = new ( stackalloc char [ 256 ] ) ;
// `ValueStringBuilder` provides a custom interpolated string handler,
// ensuring such operations do not allocate any new strings.
sb . Append ( $ "Hello, { user . Name } ! Your ID is: { user . id } " ) ;
// Unlike `StringBuilder`, `ValueStringBuilder` can be represented
// as a readonly span. Thus, you don't need to actually materialize
// the string you've built in lots of cases.
DisplayWelcome ( ( ReadOnlySpan < char > ) sb ) ;
// Remember to dispose of the builder to return
// a rented buffer, if any, back to the pool.
sb . Dispose ( ) ; ValueList<T> é uma reimplementação da List<T> projetada para suportar buffers alocados em pilha. É capaz de utilizar um pool de matriz compartilhado para expandir seu buffer interno quando necessário. ValueList<T> é perfeito para processar pequenas quantidades de dados que podem se encaixar na pilha; No entanto, não deve ser usado para qualquer outra coisa, porque as operações em conjuntos de dados maiores podem e degradarão o desempenho geral do seu aplicativo.
ValueList<T> reflete todos os recursos da List<T> e passa com sucesso o mesmo conjunto de testes de unidade, permitindo que ele sirva perfeitamente como substituição na maioria dos cenários.
// Note that providing a capacity instead of a buffer will force
// the list to rent an array from `ArrayPool<T>.Shared`.
ValueList < int > list = new ( stackalloc int [ 10 ] ) ;
list . Add ( 0 ) ;
list . Add ( 1 ) ;
list . Add ( 2 ) ;
DoSomethingWithIntegers ( ( ReadOnlySpan < int > ) list ) ;
// Remember to dispose of the list to return
// a rented buffer, if any, back to the pool.
list . Dispose ( ) ; ValueStack<T> é uma reimplementação da Stack<T> projetada para suportar buffers alocados em pilha. É capaz de utilizar um pool de matriz compartilhado para expandir seu buffer interno quando necessário. ValueStack<T> é perfeito para processar pequenas quantidades de dados que podem caber na pilha; No entanto, não deve ser usado para qualquer outra coisa, porque as operações em conjuntos de dados maiores podem e degradarão o desempenho geral do seu aplicativo.
ValueStack<T> reflete todos os recursos da Stack<T> e passa com sucesso o mesmo conjunto de testes de unidade, permitindo que ele sirva perfeitamente como substituição na maioria dos cenários.
// Note that providing a capacity instead of a buffer will force
// the stack to rent an array from `ArrayPool<T>.Shared`.
ValueStack < int > stack = new ( stackalloc int [ 10 ] ) ;
stack . Push ( 0 ) ;
stack . Push ( 1 ) ;
stack . Push ( 2 ) ;
DoSomethingWithIntegers ( ( ReadOnlySpan < int > ) stack ) ;
// Remember to dispose of the stack to return
// a rented buffer, if any, back to the pool.
stack . Dispose ( ) ; ValueQueue<T> é uma reimplementação da Queue<T> projetada para suportar buffers alocados em pilha. É capaz de utilizar um pool de matriz compartilhado para expandir seu buffer interno quando necessário. ValueQueue<T> é perfeito para processar pequenas quantidades de dados que podem se encaixar na pilha; No entanto, não deve ser usado para qualquer outra coisa, porque as operações em conjuntos de dados maiores podem e degradarão o desempenho geral do seu aplicativo.
ValueQueue<T> reflete todos os recursos da Queue<T> e passa com sucesso o mesmo conjunto de testes de unidade, permitindo que ele sirva perfeitamente como substituição na maioria dos cenários.
// Note that providing a capacity instead of a buffer will force
// the queue to rent an array from `ArrayPool<T>.Shared`.
ValueQueue < int > queue = new ( stackalloc int [ 10 ] ) ;
queue . Enqueue ( 0 ) ;
queue . Enqueue ( 1 ) ;
queue . Enqueue ( 2 ) ;
DoSomethingWithIntegers ( ( ReadOnlySpan < int > ) queue ) ;
// Remember to dispose of the queue to return
// a rented buffer, if any, back to the pool.
queue . Dispose ( ) ; ValueSet<T> é uma reimplementação de HashSet<T> projetada para suportar buffers alocados em pilha. É capaz de utilizar um pool de matriz compartilhado para expandir seu buffer interno quando necessário. ValueSet<T> é perfeito para processar pequenas quantidades de dados que podem se encaixar na pilha; No entanto, não deve ser usado para qualquer outra coisa, porque as operações em conjuntos de dados maiores podem e degradarão o desempenho geral do seu aplicativo.
ValueSet<T> reflete todos os recursos do HashSet<T> e passa com sucesso o mesmo conjunto de testes de unidade, permitindo que ele sirva perfeitamente como substituição na maioria dos cenários.
// Note that providing a capacity instead of a buffer will force
// the set to rent an array from `ArrayPool<T>.Shared`.
ValueSet < int > set = new ( stackalloc int [ 10 ] ) ;
set . Add ( 0 ) ;
set . Add ( 1 ) ;
set . Add ( 2 ) ;
DoSomethingWithIntegers ( ( ReadOnlySpan < int > ) set ) ;
// Remember to dispose of the set to return
// a rented buffer, if any, back to the pool.
set . Dispose ( ) ; ValueDictionary<TKey, TValue> é uma reimplementação do Dictionary<TKey, TValue> projetado para oferecer suporte a buffers alocados em pilha. É capaz de utilizar um pool de matriz compartilhado para expandir seu buffer interno quando necessário. ValueDictionary<TKey, TValue> é perfeito para processar pequenas quantidades de dados que podem se encaixar na pilha; No entanto, não deve ser usado para qualquer outra coisa, porque as operações em conjuntos de dados maiores podem e degradarão o desempenho geral do seu aplicativo.
ValueDictionary<TKey, TValue> reflete todos os recursos do Dictionary<TKey, TValue> e passa com sucesso o mesmo conjunto de testes de unidade, permitindo que ele sirva perfeitamente como substituição na maioria dos cenários.
// Note that providing a capacity instead of a buffer will force
// the dictionary to rent an array from `ArrayPool<T>.Shared`.
ValueDictionary < int , string > dictionary = new ( 10 ) ;
dictionary . Add ( 0 , "zero" ) ;
dictionary . Add ( 1 , "one" ) ;
dictionary . Add ( 2 , "two" ) ;
DoSomethingWithPairs ( ( ReadOnlySpan < KeyValuePair < int , string > > ) dictionary ) ;
// Remember to dispose of the dictionary to return
// a rented buffer, if any, back to the pool.
dictionary . Dispose ( ) ; .Min() e .Max() são métodos de extensão que podem ajudá -lo a encontrar o valor mínimo/máximo em um período. Eles são vetorizados para todos os tipos suportados, diferentemente Enumerable.Min() e Enumerable.Max() , que não fornecem otimizações para números de ponto flutuante.
No entanto, há um pequeno problema com os números de ponto flutuante (ou seja, float e double ), e o nome desse problema é NaN . Como você deve saber, NaN não é maior que nem menor que qualquer número, e não é igual a qualquer número, mesmo para si mesmo. Assim, se uma NaN estiver presente na sequência fornecida, poderá interromper uma implementação ingênua que se baseia apenas no resultado de operações regulares de comparação. Assim, se uma NaN estiver presente na sequência fornecida, poderá interromper uma implementação ingênua que se baseia apenas no resultado de operações regulares de comparação. Portanto, não responsabilizar essa desgraça de comparações de ponto flutuante não é uma opção.
Spanned consegue empregar todos os cheques relacionados NaN de maneira altamente eficiente, fornecendo um impulso significativo de desempenho em relação às soluções não otimizadas. No entanto, o desempenho pode ser ainda melhor se não precisássemos explicar NaN s. É por isso que .UnsafeMin() e .UnsafeMax() existem. Esses métodos são específicos para vãos contendo números de ponto flutuante e realizam operações de comparação sem reconhecer a existência de NaN , eliminando todas as verificações relacionadas. Portanto, se você tem certeza absoluta de um período de ponto flutuante, é higienizado e não pode conter nenhuma NaN s, você pode espremer ainda mais desempenho das operações .Min() e .Max() .
Embora a diferença entre .Min() e .UnsafeMin() possa não ser muito perceptível:
| Método | Significar | Stddev | Razão | Tamanho do código |
|---|---|---|---|---|
| Min_loop_single | 3.919,5 ns | 15,75 ns | 1,00 | 207 b |
| Min_linq_single | 4.030,3 ns | 37,38 ns | 1.03 | 570 b |
| Min_span_single | 611.1 ns | 8,55 ns | 0,16 | 534 b |
| Insafemin_span_single | 569,0 ns | 1,82 ns | 0,15 | 319 b |
A diferença de desempenho se torna bastante substancial entre .Max() e .UnsafeMax() :
| Método | Significar | Stddev | Razão | Tamanho do código |
|---|---|---|---|---|
| Max_loop_single | 3.849,2 ns | 36.97 ns | 1,00 | 215 b |
| Max_linq_single | 3.936,4 ns | 53,51 ns | 1.02 | 643 b |
| Max_span_single | 901,7 ns | 7.12 ns | 0,23 | 606 b |
| Insafemax_span_single | 551,8 ns | 3.06 ns | 0,14 | 321 b |
.Sum() é um método de extensão que pode ajudá -lo a calcular a soma de todos os valores em um período. Ele é vetorizado para todos os tipos suportados, diferentemente Enumerable.Sum() , que não apenas carece de vetorização, mas não fornece sobrecarga para a maioria dos tipos numéricos fora da caixa.
Semelhante a .Min() e .Max() , .Sum() tem o gêmeo maligno que passa pelo nome .UnsafeSum() . O método base lançará uma OverflowException se o cálculo da soma resultar em excesso/subflow inteiro. Os guardas de transbordamento, é claro, têm um custo e não é desprezível. Portanto, se sua entrada for higienizada e não puder causar um excesso, ou se o excesso inteiro for o comportamento esperado em seu contexto de trabalho, sinta -se à vontade para usar .UnsafeSum() . É duas vezes mais rápido que .Sum() , 34 vezes mais rápido do que calcular a soma dentro de um loop e 130 vezes casual mais rápido do que calcular a soma via LINQ:
| Método | Significar | Stddev | Razão | Tamanho do código |
|---|---|---|---|---|
| SUM_LOOP_INT16 | 3.820,0 ns | 7.04 ns | 1,00 | 128 b |
| SUM_LINQ_INT16 | 14.472,6 ns | 281.83 ns | 3.80 | 732 b |
| SUM_SPAN_INT16 | 214.6 ns | 2.43 ns | 0,06 | 413 b |
| Insefesum_span_int16 | 111,8 ns | 1,00 ns | 0,03 | 200 b |
.LongSum() é um método de extensão que pode ajudá-lo a calcular a soma de todos os valores em um período usando um acumulador de 64 bits (ou seja, long para números inteiros assinados, ulong para números inteiros não assinados e double para o float ) , capaz de armazenar um resultado maior que o valor máximo/mínimo do original int.MaxValue + int.MaxValue GEG, você não pode armazenar o int ) . Ele é vetorizado para todos os tipos suportados e não possui alternativas adequadas no LINQ (portanto, a referência abaixo é um pouco injusta) .
.LongSum() não possui uma contraparte "insegura", porque mesmo a maior extensão possível que armazena elementos int.MaxValue do tipo int não podem causar um estouro de um acumulador de 64 bits ( (long)int.MaxValue * (long)int.MaxValue < long.MaxValue ).
| Método | Significar | Stddev | Razão | Tamanho do código |
|---|---|---|---|---|
| LONGSUM_LOOP_INT16 | 2.537,1 ns | 21.30 ns | 1,00 | 98 b |
| LONGSUM_LINQ_INT16 | 14.372,0 ns | 130,00 ns | 5.67 | 734 b |
| LONGSUM_SPAN_INT16 | 251,0 ns | 2.38 ns | 0,10 | 390 b |
.Average() é um método de extensão que pode ajudá -lo a calcular a média de todos os valores em um período. Ele é vetorizado para todos os tipos suportados, diferentemente Enumerable.Average() , que fornece apenas algum nível de otimização para números inteiros assinados por 32 bits (ou seja, int s).
Sob o capô, .Average() usa .LongSum() para calcular a soma de todos os elementos, evitando transbordamentos inteiros. No entanto, se sua entrada for higienizada e não puder causar uma, você poderá mudar para .UnsafeAverage() , que usa .UnsafeSum() e não gasta o precioso tempo de execução em guardas de transbordamento.
| Método | Significar | Stddev | Razão | Tamanho do código |
|---|---|---|---|---|
| Média_loop_int16 | 2.482,1 ns | 20.04 ns | 1,00 | 241 b |
| Média_linq_int16 | 13.198.2 ns | 97,67 ns | 5.31 | 1.016 b |
| Média_span_int16 | 257,8 ns | 3,61 ns | 0,10 | 593 b |
| Inselaverage_span_int16 | 116,7 ns | 1,27 ns | 0,05 | 128 b |
.FillSequential() é um método de extensão que pode ajudá -lo a preencher um determinado período com valores numéricos seqüenciais. É vetorizado para todos os tipos suportados e não possui alternativas no LINQ.
| Método | Significar | Stddev | Razão | Tamanho do código |
|---|---|---|---|---|
| FILLEQUENTAL_LOOP_INT16 | 2.499,4 ns | 28.47 ns | 1,00 | 118 b |
| FILLEQUENTAL_SPAN_INT16 | 169,2 ns | 0,18 ns | 0,07 | 660 b |
.IndexOf() , .LastIndexOf() e .Contains() podem parecer familiares para você, porque esses métodos são fornecidos pelo MemoryExtensions . No entanto, existem dois problemas com eles:
IEquatable<T> , tornando -os inacessíveis no contexto genérico não ligado.IEqualityComparer<T> .As implementações desses métodos fornecidos por este biblioteca abordam esses dois problemas.
Licenciado nos termos da licença do MIT.