← Назад к вопросам

Что такое Span<T> и Memory<T> в C#? Когда их использовать для оптимизации производительности?

2.0 Middle🔥 201 комментариев
#Основы C# и .NET

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI7 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Span<T> и Memory<T>: Обзор

Span<T> и Memory<T> — это типы, введенные в C# 7.2 (.NET Core 2.1+) для работы с непрерывными областями памяти с минимальными накладными расходами. Они позволяют работать с управляемыми массивами, неуправляемой памятью и стеками без лишних выделений памяти и копирования данных.

Span<T>

Span<T> — это ref struct, которая предоставляет типобезопасное представление непрерывного участка памяти. Из-за ограничений ref struct она может находиться только в стеке, что делает ее идеальной для высокопроизводительных сценариев.

// Работа с массивом через Span
byte[] data = new byte[100];
Span<byte> span = data.AsSpan();
span[0] = 1;

// Работа со стеком через stackalloc
Span<int> stackSpan = stackalloc int[10];
stackSpan[0] = 42;

// Работа с неуправляемой памятью
IntPtr nativeMemory = Marshal.AllocHGlobal(100);
Span<byte> nativeSpan;
unsafe {
    nativeSpan = new Span<byte>(nativeMemory.ToPointer(), 100);
}

Memory<T>

Memory<T> — это структура, которая оборачивает Span<T>, но не имеет ограничений ref struct. Ее можно использовать в асинхронных методах, полях классов и коллекциях.

// Memory можно использовать в асинхронных операциях
async Task ProcessData(Memory<byte> memory) {
    await Task.Delay(100);
    memory.Span[0] = 100;
}

// Memory можно хранить в полях класса
class Buffer {
    private Memory<byte> _buffer;
    
    public Buffer(Memory<byte> buffer) {
        _buffer = buffer;
    }
}

Ключевые различия

ХарактеристикаSpan<T>Memory<T>
РасположениеТолько стекКуча и стек
Использование в asyncНельзяМожно
Поля классаЗапрещеноРазрешено
ПроизводительностьМаксимальнаяНемного ниже

Когда использовать для оптимизации

1. Работа с массивами без копирования

Вместо создания копий массивов используйте срезы через Span/Memory:

// ПЛОХО: создается новая копия массива
byte[] GetSubarray(byte[] source, int start, int length) {
    var result = new byte[length];
    Array.Copy(source, start, result, 0, length);
    return result;
}

// ХОРОШО: без копирования
Span<byte> GetSubspan(byte[] source, int start, int length) {
    return source.AsSpan(start, length);
}

2. Парсинг и обработка строк

Span<T> революционизировал парсинг в .NET:

// Оптимизированный парсинг чисел
int ParseInt(ReadOnlySpan<char> span) {
    int result = 0;
    for (int i = 0; i < span.Length; i++) {
        result = result * 10 + (span[i] - '0');
    }
    return result;
}

// Без аллокаций при разделении строки
string text = "a,b,c,d,e";
ReadOnlySpan<char> textSpan = text.AsSpan();
foreach (var segment in textSpan.Split(',')) {
    // Обработка каждого сегмента без аллокаций
}

3. Работа с сетевыми протоколами и файлами

При чтении данных из сетевых потоков или файлов:

async Task ProcessStream(Stream stream) {
    using IMemoryOwner<byte> owner = MemoryPool<byte>.Shared.Rent(4096);
    Memory<byte> buffer = owner.Memory;
    
    int bytesRead;
    while ((bytesRead = await stream.ReadAsync(buffer)) > 0) {
        ProcessBuffer(buffer.Slice(0, bytesRead));
    }
}

void ProcessBuffer(Memory<byte> buffer) {
    Span<byte> span = buffer.Span;
    // Обработка данных без копирования
}

4. Высокопроизводительные алгоритмы

Для математических вычислений, обработки изображений, криптографии:

// Векторные операции над массивами
void AddVectors(Span<float> left, Span<float> right, Span<float> result) {
    for (int i = 0; i < left.Length; i++) {
        result[i] = left[i] + right[i];
    }
}

// Использование SIMD инструкций
void SIMDAdd(Span<float> left, Span<float> right, Span<float> result) {
    if (Vector.IsHardwareAccelerated) {
        var count = left.Length / Vector<float>.Count;
        for (int i = 0; i < count; i++) {
            var v1 = new Vector<float>(left.Slice(i * Vector<float>.Count));
            var v2 = new Vector<float>(right.Slice(i * Vector<float>.Count));
            (v1 + v2).CopyTo(result.Slice(i * Vector<float>.Count));
        }
    }
}

Практические рекомендации

Когда использовать Span<T>:

  • Методы, работающие только в синхронном контексте
  • Критичные к производительности горячие пути
  • Методы, которые не требуют сохранения состояния
  • Стек-аллоцированные буферы через stackalloc

Когда использовать Memory<T>:

  • Асинхронные операции и методы
  • Поля классов и структур
  • Долгоживущие ссылки на данные
  • Работа с пулами памяти (MemoryPool)

Важные ограничения:

  1. Span<T> нельзя использовать в асинхронных методах
  2. Span<T> нельзя хранить в полях классов
  3. Span<T> нельзя использовать в итераторах (yield)
  4. Span<T> нельзя использовать в лямбда-выражениях, захватывающих переменные

Пример комплексной оптимизации

public class JsonParser {
    // Используем Memory для хранения буфера в классе
    private Memory<char> _buffer;
    
    public async Task ParseAsync(Stream stream) {
        using var owner = MemoryPool<char>.Shared.Rent(8192);
        _buffer = owner.Memory;
        
        int charsRead;
        while ((charsRead = await ReadStream(stream)) > 0) {
            // Используем Span для высокопроизводительной обработки
            ProcessChunk(_buffer.Span.Slice(0, charsRead));
        }
    }
    
    private void ProcessChunk(ReadOnlySpan<char> chunk) {
        // Высокопроизводительный парсинг без аллокаций
        int index = 0;
        while (index < chunk.Length) {
            if (chunk[index] == '"') {
                index = ParseString(chunk, index + 1);
            }
            index++;
        }
    }
    
    private int ParseString(ReadOnlySpan<char> chunk, int start) {
        // Анализ строки без создания подстрок
        int end = start;
        while (end < chunk.Length && chunk[end] != '"') {
            end++;
        }
        return end;
    }
}

Заключение

Span<T> и Memory<T> — это мощные инструменты для оптимизации производительности в C#, которые устраняют необходимость в лишних аллокациях и копированиях памяти. Их правильное использование позволяет:

  • Уменьшить нагрузку на GC
  • Увеличить скорость обработки данных
  • Снизить потребление памяти
  • Улучшить локализацию кэша процессора

Однако важно понимать их ограничения и использовать в соответствии с требованиями конкретного сценария. Для синхронных, высокопроизводительных операций выбирайте Span<T>, для асинхронных и долгоживущих сценариев — Memory<T>.

Что такое Span<T> и Memory<T> в C#? Когда их использовать для оптимизации производительности? | PrepBro