В чем разница между Span<T> и Memory<T> в C# 7.2?
C# 7.2 вводит два новых типа: Span<T>
а также Memory<T>
которые имеют лучшую производительность по сравнению с более ранними типами C#, такими как string[]
,
Вопрос: в чем разница между Span<T>
а также Memory<T>
? Зачем мне использовать один над другим?
4 ответа
Span<T>
только для стека Memory<T>
можно использовать кучу.
Span<T>
это новый тип, который мы добавляем в платформу для представления смежных областей произвольной памяти с характеристиками производительности на уровне T[]. Его API-интерфейсы похожи на массив, но в отличие от массивов, он может указывать либо на управляемую, либо на собственную память, либо на память, выделенную в стеке.
Memory <T>
это тип, дополняющийSpan<T>
, Как обсуждено в его проектной документации,Span<T>
тип только для стека Только для стекаSpan<T>
делает его непригодным для многих сценариев, которые требуют хранения ссылок на буферы (представленыSpan<T>
) в куче, например, для подпрограмм, выполняющих асинхронные вызовы.
async Task DoSomethingAsync(Span<byte> buffer) {
buffer[0] = 0;
await Something(); // Oops! The stack unwinds here, but the buffer below
// cannot survive the continuation.
buffer[0] = 1;
}
Для решения этой проблемы мы предоставим набор дополнительных типов, предназначенных для использования в качестве типов обмена общего назначения, представляющих, как
Span <T>
Диапазон произвольной памяти, но в отличиеSpan <T>
эти типы не будут только стековыми, за счет значительных потерь производительности при чтении и записи в память.
async Task DoSomethingAsync(Memory<byte> buffer) {
buffer.Span[0] = 0;
await Something(); // The stack unwinds here, but it's OK as Memory<T> is
// just like any other type.
buffer.Span[0] = 1;
}
В приведенном выше примере
Memory <byte>
используется для представления буфера. Это обычный тип и может использоваться в методах, выполняющих асинхронные вызовы. Возвращает его свойство SpanSpan<byte>
, но возвращаемое значение не сохраняется в куче во время асинхронных вызовов, а новые значения создаются изMemory<T>
значение. В некотором смыслеMemory<T>
это фабрикаSpan<T>
,
Справочный документ: здесь
re: это означает, что он может указывать только на память, выделенную в стеке.
Span<T>
может указывать на любую память: выделенную либо в стеке, либо в куче. Стек только природа Span<T>
означает, что Span<T>
сама (а не память, на которую она указывает) может находиться только в стеке. Это в отличие от "обычных" структур C#, которые могут находиться в стеке или в куче (когда они встроены в классы / ссылочные типы). Наиболее очевидные практические последствия состоят в том, что вы не можете иметь Span<T>
поле в классе, вы не можете боксировать Span<T>
и вы не можете поместить их в массивы.
Общий
Span определяется как ссылочная структура а Memory определяется как структура,
Что это значит?
Ссылочные структуры не могут храниться в куче, компилятор не позволит вам это сделать, поэтому следующее не будет разрешено:
- Использование Span в качестве поля в классе
- Использование Span в асинхронном методе (асинхронные методы расширяются в полноценный конечный автомат)
- и многое другое, вот полный список вещей, которые нельзя сделать со ссылочными структурами.
stackalloc не будет работать с памятью (потому что нет гарантии, что он не будет храниться в куче), но будет работать с Span
// this is legit Span<byte> data = stackalloc byte[256]; // legit // compile time error: Conversion of a stackalloc expression of type 'byte' to type 'Memory<byte>' is not possible. Memory<byte> data = stackalloc byte[256];
Что это означает?
Это означает, что в некоторых сценариях различные микрооптимизации невозможны с самим Span , и поэтому вместо него следует использовать память .
Пример:
Вот пример метода Split без выделения строки , который работает со структурой ReadOnlyMemory , реализовать это на Span было бы очень сложно из-за того, что Span является ссылочной структурой и не может быть помещен в массив или IEnumerable:
(Реализация взята из C# в краткой книге )
IEnumerable<ReadOnlyMemory<char>> Split(ReadOnlyMemory<char> input)
{
int wordStart = 0;
for (int i = 0; i <= input.Length; i++)
{
if (i == input.Length || char.IsWhiteSpace(input.Span[i]))
{
yield return input.Slice(wordStart, i);
wordStart = i + 1;
}
}
}
А вот результаты очень простого теста с помощью библиотеки тестов dotnet по сравнению с обычным методом Split на .NET SDK=6.0.403.
| Method | StringUnderTest | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated |
|---------------------- |-------------------- |------------------:|------------------:|------------------:|----------:|----------:|---------:|-----------:|
| RegularSplit | meow | 13.194 ns | 0.2891 ns | 0.3656 ns | 0.0051 | - | - | 32 B |
| SplitOnReadOnlyMemory | meow | 8.991 ns | 0.1981 ns | 0.2433 ns | 0.0127 | - | - | 80 B |
| RegularSplit | meow(...)meow [499] | 1,077.807 ns | 21.2291 ns | 34.8801 ns | 0.6409 | 0.0095 | - | 4024 B |
| SplitOnReadOnlyMemory | meow(...)meow [499] | 9.036 ns | 0.2055 ns | 0.2366 ns | 0.0127 | - | - | 80 B |
| RegularSplit | meo(...)eow [49999] | 121,740.719 ns | 2,221.3079 ns | 2,077.8128 ns | 63.4766 | 18.5547 | - | 400024 B |
| SplitOnReadOnlyMemory | meo(...)eow [49999] | 9.048 ns | 0.2033 ns | 0.2782 ns | 0.0127 | - | - | 80 B |
| RegularSplit | me(...)ow [4999999] | 67,502,918.403 ns | 1,252,689.2949 ns | 2,092,962.4006 ns | 5625.0000 | 2375.0000 | 750.0000 | 40000642 B |
| SplitOnReadOnlyMemory | me(...)ow [4999999] | 9.160 ns | 0.2057 ns | 0.2286 ns | 0.0127 | - | - | 80 B |
Входными данными для этих методов были строки «мяу», повторяющиеся 1, 100, 10_000 и 1_000_000 раз, моя настройка теста не была идеальной, но она показывает разницу.
Memory<T>
можно рассматривать как небезопасную, но более универсальную версию Span<T>
, Доступ к Memory<T>
Объект потерпит неудачу, если он указывает на освобожденный массив.