Spanned высокопроизводительная библиотека с нулевым распределением .NET, которая вводит совместимые с SPAN альтернативы популярным типам BCL и предоставляет векторизованные решения для общих операций на промежутках.
Чтобы начать, сначала добавьте пакет в ваш проект. Вы можете сделать это, выполнив следующую команду:
dotnet add package SpannedВ качестве альтернативы, вы можете установить его через консоль диспетчера пакетов с этой командой:
Install-Package SpannedОбратите внимание, что .NET со временем накопил многочисленные маршруты оптимизации, как в зависимости от фреймворта и среды. Следовательно, стало чрезвычайно сложным для соседа высоко оптимизированного кода для разных структур в одной кодовой базе. Поэтому я принял трудное решение поддержать только одну структуру на версию этой библиотеки. Вы можете найти список версий библиотеки и соответствующие поддерживаемые фрейв -версии в таблице ниже:
| .NET Стандарт 2.0 | .NET Стандарт 2.1 | .Net 8+ | |
|---|---|---|---|
Хотя может быть заманчиво использовать v0.0.1 для всех ваших потребностей, учитывая его поддержку .NET Standard 2.0 и широкое распространение .NET Standard 2.0 в настоящее время, я настоятельно рекомендую против этого. Практически нет оптимизации, которые можно выполнить, используя эту устаревшую структуру. Даже наш любимый Span , доступный в .net Standard 2.0 через пакет System.Memory Nuget, известен как «медленный пролет», потому что этот Span -не что иное, как повторно внесенный ArraySegment , не имеющий надлежащей поддержки на стороне выполнения/JIT. Поэтому, пожалуйста, выберите лучшую версию пакета для вашей среды, а не та, которая, кажется, подходит для всех.
Прежде чем мы рассмотрим подробности, давайте обсудим некоторые общие случаи края, с которыми вы можете столкнуться при использовании этой библиотеки, и ответим на вопрос: «Почему X часть .NET?» Короче говоря, все, что вы можете найти здесь, легко использовать и легко злоупотреблять.
Давайте начнем с очевидного момента. Эта библиотека разработана специально для сценариев, в которых вы боретесь с зубом и гвозди за каждый выделенный байт и каждую наносекунду времени исполнения на очень критических путях. Это не предназначено для того, чтобы быть универсальным решением для всей вашей кодовой базы. Безусловно включение типов из этой библиотеки в каждом мыслимом сценарии может ухудшить общую производительность вашего приложения, а не улучшать его.
Помните, что не погружайтесь сначала в океан нанооптимизации, пока не уверен, что это необходимо.
Распространенной ошибкой, которой следует избегать, является передача любых типов, предоставленных этой библиотекой по цене (да, это одно должно помочь вам понять, почему что -то подобное не должно и никогда не будет частью BCL) . Например, хотя следующий код может показаться нормальным, он на самом деле катастрофический:
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 ) ;
} Добавление к строителю строк может увеличить свой внутренний буфер. Однако, поскольку мы прошли наш ValueStringBuilder по значению (то есть скопировали его) , первоначальный ValueStringBuilder не будет знать об этом и будет продолжать использовать уже утилизированный буфер.
Несмотря на то, что этот подход может работать с дезинфицированными входными данными во время тестирования, он иногда терпит неудачу, разбивая не только ваш код, но и некоторые случайные части времени выполнения вашего приложения, вмешиваясь в буфер, который копия ValueStringBuilder уже вернулась в пул, поэтому его можно повторно использовано чем -то другим.
Вы можете попытаться быть умным в этом и решить проблему, переписав метод проблемного расширения следующим образом:
public static void AppendUserName ( this in ValueStringBuilder sb , User user )
{
sb . Append ( user . FirstName ) ;
sb . Append ( ' ' ) ;
sb . Append ( user . LastName ) ;
} Теперь ValueStringBuilder передается с помощью ссылки, так что не должно быть никаких проблем, верно? Ну нет. Существует in для снижения затрат на копирование всего экземпляра типа значения путем передачи его ссылки на метод при сохранении семантики, как если бы он был передан по значению. Это означает, что любые изменения состояния предоставленного ValueStringBuilder не будут распространяться в исходный экземпляр. Итак, у нас все еще есть такая же проблема в наших руках. Единственный правильный способ реализации метода, который может изменить внутреннее состояние экземпляра типа значения, - это фактически передавая его ссылкой:
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 ) ;
}Хотя это не так причудливое, как некоторые хотели бы, чтобы это было, это решение имеет преимущество фактической работы.
Большинство типов, предоставленных этой библиотекой, определяют метод Dispose() , позволяющий использовать их с using ключевого слова, как можно увидеть ниже:
using ValueStringBuilder sb = new ValueStringBuilder ( stackalloc char [ 16 ] ) ;
Foo ( ref sb ) ;
return sb . ToString ( ) ; Однако это не означает, что они должны использоваться с using ключевого слова. Очень важно помнить, как приведенный выше код фактически снижается:
ValueStringBuilder sb = new ValueStringBuilder ( stackalloc char [ 16 ] ) ;
try
{
Foo ( ref sb ) ;
return sb . ToString ( ) ;
}
finally
{
sb . Dispose ( ) ;
} Создание и управление защищенными регионами не бесплатно. Учитывая наше внимание на нанооптимизации, влияние здесь заметно. Следовательно, предпочтительнее вручную вызовать Dispose() :
ValueStringBuilder sb = new ValueStringBuilder ( stackalloc char [ 16 ] ) ;
Foo ( ref sb ) ;
string result = sb . ToString ( ) ;
sb . Dispose ( ) ;
return result ;В качестве альтернативы, проверьте, есть ли последний метод, который вы вызовите на данном типе, имеет перегрузку, которая выполняет очистку автоматически:
ValueStringBuilder sb = new ValueStringBuilder ( stackalloc char [ 16 ] ) ;
Foo ( ref sb ) ;
return sb . ToString ( dispose : true ) ;В современном .NET часто встречается следующая шаблона:
Или, выражая ту же концепцию в коде:
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 ) ;
} Не самый красивый кусок шаблон, не так ли? Фактическая логика часто заканчивается от нее, которая далека от идеальной. Это точная проблема, направленная на SpanOwner , чтобы решить. Вот та же самая логика, но все парикмахерская была спрятана за SpanOwner :
SpanOwner < T > owner = SpanOwner < T > . ShouldRent ( length ) ? SpanOwner < T > . Rent ( length ) : stackalloc T [ length ] ;
Span < T > span = owner . Span ;
DoSomeWorkWithSpan ( span ) ;
owner . Dispose ( ) ; Гораздо проще писать, гораздо проще для чтения, и, что наиболее важно, этот подход обеспечивает точно такую же производительность, потому что SpanOwner предназначен для полной инлина. Это может быть полностью исключено из вашего кода JIT:
| Метод | Иметь в виду | Stddev | Соотношение | Код размер |
|---|---|---|---|---|
| Без_SPANOWNER_INT32 | 5,134 нс | 0,0425 нс | 1,00 | 315 б |
| With_spanowner_int32 | 4,908 нс | 0,0168 нс | 0,96 | 310 б |
ValueStringBuilder -это повторное внедрение StringBuilder , предназначенного для поддержки буферов с помощью стека. Он способен использовать общий бассейн массивов, чтобы при необходимости расширить свой внутренний буфер. ValueStringBuilder идеально подходит для создания компактных струн, которые могут поместиться на стеке; Тем не менее, это не должно использоваться ни для чего другого, потому что операции на более крупных строках могут и ухудшить общую производительность вашего приложения. Истинный блеск ValueStringBuilder появляется, когда вам нужно создать короткую последовательность символов, которую вообще не нужно материализовать как строку.
ValueStringBuilder отражает все функции StringBuilder и успешно проходит один и тот же набор модульных тестов, позволяя ему легко служить заменой в большинстве сценариев.
// 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> -это повторное внедрение List<T> разработанного для поддержки буферов с помощью стека. Он способен использовать общий бассейн массивов, чтобы при необходимости расширить свой внутренний буфер. ValueList<T> идеально подходит для обработки небольших объемов данных, которые могут поместиться в стеке; Тем не менее, это не должно использоваться ни для чего другого, потому что операции на более крупных наборах данных могут и ухудшить общую производительность вашего приложения.
ValueList<T> отражает все функции List<T> и успешно проходит один и тот же набор модульных тестов, позволяя ему плавно служить заменой в большинстве сценариев.
// 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> -это повторное внедрение Stack<T> предназначенное для поддержки буферов с помощью стека. Он способен использовать общий бассейн массивов, чтобы при необходимости расширить свой внутренний буфер. ValueStack<T> идеально подходит для обработки небольших объемов данных, которые могут соответствовать стеке; Тем не менее, это не должно использоваться ни для чего другого, потому что операции на более крупных наборах данных могут и ухудшить общую производительность вашего приложения.
ValueStack<T> отражает все функции Stack<T> и успешно проходит один и тот же набор модульных тестов, позволяя ему плавно служить заменой в большинстве сценариев.
// 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> -это повторное внедрение Queue<T> разработанной для поддержки буферов с помощью стека. Он способен использовать общий бассейн массивов, чтобы при необходимости расширить свой внутренний буфер. ValueQueue<T> идеально подходит для обработки небольших объемов данных, которые могут соответствовать стеке; Тем не менее, это не должно использоваться ни для чего другого, потому что операции на более крупных наборах данных могут и ухудшить общую производительность вашего приложения.
ValueQueue<T> отражает все функции Queue<T> и успешно проходит один и тот же набор модульных тестов, позволяя ему легко служить заменой в большинстве сценариев.
// 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> -это повторное внедрение HashSet<T> предназначенное для поддержки буферов с помощью стека. Он способен использовать общий бассейн массивов, чтобы при необходимости расширить свой внутренний буфер. ValueSet<T> идеально подходит для обработки небольших объемов данных, которые могут соответствовать стеке; Тем не менее, это не должно использоваться ни для чего другого, потому что операции на более крупных наборах данных могут и ухудшить общую производительность вашего приложения.
ValueSet<T> отражает все особенности HashSet<T> и успешно проходит один и тот же набор модульных тестов, позволяя ему плавно служить заменой в большинстве сценариев.
// 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> -это переопределение Dictionary<TKey, TValue> разработанное для поддержки буферов с помощью стека. Он способен использовать общий бассейн массивов, чтобы при необходимости расширить свой внутренний буфер. ValueDictionary<TKey, TValue> идеально подходит для обработки небольших объемов данных, которые могут поместиться в стеке; Тем не менее, это не должно использоваться ни для чего другого, потому что операции на более крупных наборах данных могут и ухудшить общую производительность вашего приложения.
ValueDictionary<TKey, TValue> отражает все функции Dictionary<TKey, TValue> и успешно проходит один и тот же набор модульных тестов, позволяя ему плавно служить заменой в большинстве сценариев.
// 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() и .Max() - это методы расширения, которые могут помочь вам найти минимальное/максимальное значение в промежутке. Они векторизированы для всех поддерживаемых типов, в отличие от Enumerable.Min() и Enumerable.Max() , которые не обеспечивают никакой оптимизации для чисел с плавающей точкой.
Тем не менее, существует небольшая проблема с числами с плавающей точкой (т. Е. float и double ), и название этой проблемы- NaN . Как вы, возможно, знаете, NaN не больше, чем и не меньше, чем какое -либо число, и это не равно ни одному числу, даже для себя. Таким образом, если NaN присутствует в предоставленной последовательности, он может нарушить наивную реализацию, которая зависит исключительно на результат регулярных операций сравнения. Таким образом, если NaN присутствует в предоставленной последовательности, он может нарушить наивную реализацию, которая зависит исключительно на результат регулярных операций сравнения. Следовательно, не учитывая это проклятие сравнений с плавающей точкой, не является вариантом.
Spanned удается использовать все, связанные с NaN , высокоэффективно, обеспечивая значительное повышение производительности по сравнению с нептимизированными решениями. Тем не менее, производительность может быть еще лучше, если бы нам не нужно было учитывать NaN S. Вот почему существуют .UnsafeMin() и .UnsafeMax() . Эти методы специфичны для пролетов, содержащих числа с плавающей точкой, и они выполняют операции сравнения без признания существования NaN , исключая все связанные проверки. Таким образом, если вы абсолютно уверены, что диапазон чисел с плавающей точкой дезинфицируется и не может содержать каких-либо NaN S, вы можете выжать еще большую производительность из .Min() и .Max() .
В то время как разница между .Min() и .UnsafeMin() может быть не очень заметной:
| Метод | Иметь в виду | Stddev | Соотношение | Код размер |
|---|---|---|---|---|
| Min_loop_singl | 3919,5 нс | 15,75 нс | 1,00 | 207 б |
| Min_linq_singl | 4030,3 нс | 37,38 нс | 1.03 | 570 б |
| Min_span_singl | 611,1 нс | 8,55 нс | 0,16 | 534 б |
| Uncafemin_span_singl | 569,0 нс | 1,82 нс | 0,15 | 319 б |
Разрыв в производительности становится довольно существенным между .Max() и .UnsafeMax() :
| Метод | Иметь в виду | Stddev | Соотношение | Код размер |
|---|---|---|---|---|
| Max_loop_singl | 3849,2 нс | 36,97 нс | 1,00 | 215 б |
| Max_linq_singl | 3936,4 нс | 53,51 нс | 1.02 | 643 б |
| Max_span_singl | 901,7 нс | 7,12 нс | 0,23 | 606 б |
| Uncafemax_span_singl | 551,8 нс | 3,06 нс | 0,14 | 321 б |
.Sum() - это метод расширения, который может помочь вам вычислить сумму всех значений в промежутке. Он векторизован для всех поддерживаемых типов, в отличие от Enumerable.Sum() , который не просто не хватает векторизации, но и не обеспечивает перегрузки для большинства численных типов из коробки вообще.
Похоже на .Min() и .Max() , .Sum() имеет злой близнец, который носит по имени .UnsafeSum() . Базовый метод будет выбросить OverflowException если вычисления суммы приведут к целочисленному переполнению/нижнему потоку. Охранники, конечно, стоят за переполнением, и это не незначительно. Следовательно, если ваш ввод продезинфицирован и не может вызвать переполнение или если переполнение целого числа является ожидаемым поведением в вашем рабочем контексте, не стесняйтесь использовать .UnsafeSum() . Это в два раза быстрее, чем .Sum() , в 34 раза быстрее, чем вычисление суммы внутри цикла, и случайный в 130 раз быстрее, чем вычисление суммы через LINQ:
| Метод | Иметь в виду | Stddev | Соотношение | Код размер |
|---|---|---|---|---|
| Sum_loop_int16 | 3820,0 нс | 7,04 нс | 1,00 | 128 б |
| Sum_linq_int16 | 14 472,6 нс | 281,83 нс | 3.80 | 732 б |
| Sum_span_int16 | 214,6 нс | 2,43 нс | 0,06 | 413 б |
| Unfeafesum_span_int16 | 111,8 нс | 1,00 нс | 0,03 | 200 б |
.LongSum() -это метод расширения, который может помочь вам вычислить сумму всех значений в диапазоне с использованием 64-разрядного аккумулятора (т. Е. long для подписанных целых чисел, ulong для не знаменитых целых чисел и double для float ) , способный сохранять результат, больше, чем максимальный/минимальный значение исходного int.MaxValue + int.MaxValue (э. int ) . Он векторизован для всех поддерживаемых типов и не имеет надлежащих альтернатив в LINQ (таким образом, эталон ниже является немного несправедливым) .
.LongSum() не имеет «небезопасного» аналога, потому что даже самый большой возможный пролет, который хранит int.MaxValue элементы типа int , не может вызвать переполнение 64-битного аккумулятора ( (long)int.MaxValue * (long)int.MaxValue < long.MaxValue ).
| Метод | Иметь в виду | Stddev | Соотношение | Код размер |
|---|---|---|---|---|
| Longsum_loop_int16 | 2 537,1 нс | 21.30 нс | 1,00 | 98 б |
| Longsum_linq_int16 | 14 372,0 нс | 130,00 нс | 5.67 | 734 б |
| Longsum_span_int16 | 251,0 нс | 2,38 нс | 0,10 | 390 б |
.Average() - это метод расширения, который может помочь вам вычислить среднее значение всех значений в промежутке. Он векторен для всех поддерживаемых типов, int отличие от Enumerable.Average() .
Под капотом .Average() использует .LongSum() для вычисления суммы всех элементов, избегая протокола целочисленного целого числа. Однако, если ваш ввод дезинфицирован и не может вызвать его, вы можете переключиться на .UnsafeAverage() , который использует .UnsafeSum() и не тратит драгоценное время выполнения на охранниках переполнения.
| Метод | Иметь в виду | Stddev | Соотношение | Код размер |
|---|---|---|---|---|
| Verage_loop_int16 | 2482,1 нс | 20,04 нс | 1,00 | 241 б |
| Verage_linq_int16 | 13,198,2 нс | 97,67 нс | 5.31 | 1016 б |
| Verage_span_int16 | 257,8 нс | 3,61 нс | 0,10 | 593 б |
| Невыболевая среда_SPAN_INT16 | 116,7 нс | 1,27 нс | 0,05 | 128 б |
.FillSequential() - это метод расширения, который может помочь вам заполнить заданный промежуток последовательными числовыми значениями. Он векторен для всех поддерживаемых типов и не имеет альтернатив в LINQ.
| Метод | Иметь в виду | Stddev | Соотношение | Код размер |
|---|---|---|---|---|
| Fill SecureCential_loop_int16 | 2499,4 нс | 28,47 нс | 1,00 | 118 б |
| Fill Secutientent_span_int16 | 169,2 нс | 0,18 нс | 0,07 | 660 б |
.IndexOf() , .LastIndexOf() и .Contains() может показаться вам знакомым, потому что эти методы предоставляются MemoryExtensions . Однако с ними есть две проблемы:
IEquatable<T> , делая их недоступными в несвязанном общем контексте.IEqualityComparer<T> .Реализации этих методов, предоставленных этой библиотекой, решают обе эти проблемы.
Лицензирован в соответствии с условиями лицензии MIT.