Поделиться через


Сокращение выделения памяти с помощью новых функций C#

Это важно

Описанные в этом разделе методы повышают производительность при применении к горячим путям в коде. Горячие пути — это те разделы базы кода, которые выполняются часто и многократно в обычных операциях. Применение этих методов к коду, который не часто выполняется, будет иметь минимальное влияние. Перед внесением изменений для улучшения производительности важно измерить исходный уровень. Затем проанализируйте базовые показатели, чтобы определить, где происходят узкие места памяти. Вы можете узнать о многих кроссплатформенных средствах для измерения производительности приложения в разделе "Диагностика и инструментирование". Вы можете практиковать сеанс профилирования в руководстве по измерению использования памяти в документации По Visual Studio.

После измерения использования памяти и определения того, что можно уменьшить объем выделений, используйте методы, описанные в этом разделе, чтобы сократить объем выделений. После каждого последовательного изменения снова измеряйте использование памяти. Убедитесь, что каждое изменение оказывает положительное влияние на использование памяти в приложении.

Производительность работы в .NET часто означает удаление выделений из кода. Каждый блок памяти, который вы выделяете, в конечном итоге должен быть освобожден. Уменьшение количества выделений сокращает время, потраченное на сбор мусора. Это позволяет более предсказуемо контролировать время выполнения, удалив сборщики мусора из определенных участков кода.

Распространенная тактика сокращения выделения заключается в изменении критически важных структур данных с class типов на struct типы. Это изменение влияет на семантику использования этих типов. Параметры и возвращаемые значения теперь передаются по значению, вместо ссылки. Стоимость копирования значения незначительна, если объем типов мал — три слова или меньше (учитывая, что размер одного слова соответствует размеру одного целого числа). Это измеряемое значение, которое может реально повлиять на производительность крупных типов данных. Чтобы бороться с эффектом копирования, разработчики могут передать эти типы через ref, чтобы сохранить их предполагаемую семантику.

Функции C# ref позволяют выразить нужную семантику для struct типов, не влияя на общую удобство использования. До этих улучшений разработчикам необходимо использовать unsafe конструкции с указателями и необработанной памятью, чтобы добиться того же влияния на производительность. Компилятор создает проверенный безопасный код для новых ref связанных функций. Проверенный безопасный код означает, что компилятор обнаруживает возможные переполнения буфера или доступ к нераспределенной или освобожденной памяти. Компилятор обнаруживает и предотвращает некоторые ошибки.

Передача и возврат по ссылке

Переменные в C# хранят значения. В struct типах значение — это содержимое экземпляра типа. В class типах значение является ссылкой на блок памяти, в который хранится экземпляр типа. ref Добавление модификатора означает, что переменная сохраняет ссылку на значение. В struct типах ссылка указывает на хранилище, содержащее значение. В class типах ссылка указывает на хранилище, содержащее ссылку на блок памяти.

В C#параметры методов передаются по значению, а возвращаемые значения возвращаются по значению. Значение аргумента передается методу. Значение возвращаемого аргумента — возвращаемое значение.

Модификатор ref, in, ref readonly, или out указывает, что аргумент передается по ссылке. Ссылка на расположение хранилища передается методу. Добавление ref к сигнатуре метода означает, что возвращаемое значение возвращается по ссылке. Ссылка на расположение хранилища — это возвращаемое значение.

Вы также можете использовать назначение ссылок, чтобы переменная ссылалась на другую переменную. Обычное присваивание копирует значение правой стороны в переменную на левой стороне присваивания. Присваивание ссылки копирует адрес памяти переменной справа в переменную слева. Теперь ref ссылается на исходную переменную:

int anInteger = 42; // assignment.
ref int location = ref anInteger; // ref assignment.
ref int sameLocation = ref location; // ref assignment

Console.WriteLine(location); // output: 42

sameLocation = 19; // assignment

Console.WriteLine(anInteger); // output: 19

При назначении переменной измените его значение. При назначении ссылки переменной, вы изменяете то, на что она ссылается.

Вы можете работать непосредственно с хранилищем для значений, используя ref переменные, передачу параметров по ссылке и присвоение с помощью ref. Правила области, применяемые компилятором, обеспечивают безопасность при работе непосредственно с хранилищем.

ref readonly И in модификаторы указывают, что аргумент должен передаваться по ссылке и не может быть переназначирован в методе. Разница заключается в том, что ref readonly метод использует параметр в качестве переменной. Метод может использовать параметр или вернуть его через ссылку только для чтения. В этих случаях следует использовать ref readonly модификатор. in В противном случае модификатор обеспечивает большую гибкость. Не нужно добавлять модификатор in к аргументу для параметра in, так что вы можете безопасно обновлять существующие сигнатуры API, используя модификатор in. Компилятор выдает предупреждение, если вы не добавляете ref или in модификатор в аргумент параметра ref readonly .

Безопасный контекст ссылки

C# содержит правила для ref выражений, чтобы ref выражение не могло быть доступным, когда хранилище, на которое оно ссылается, больше не является действительным. Рассмотрим следующий пример:

public ref int CantEscape()
{
    int index = 42;
    return ref index; // Error: index's ref safe context is the body of CantEscape
}

Компилятор сообщает об ошибке, так как невозможно вернуть ссылку на локальную переменную из метода. Вызывающий объект не может получить доступ к указанному хранилищу. Безопасный контекст ссылки определяет область, в которой ref выражение безопасно для доступа или изменения. В следующей таблице перечислены безопасные контексты ссылок для типов переменных. ref поля не могут быть объявлены в class или в struct без ссылки, поэтому эти строки не указаны в таблице.

Декларация Безопасный контекст ссылок
локальный без ссылки блок, где объявлена локальная переменная
Параметр, отличный от ссылок текущий метод
ref, ref readonly, in параметр метод вызова
out параметр текущий метод
Полеclass метод вызова
Поле, не являющееся ссылкой struct текущий метод
ref поле ref struct метод вызова

Переменная может быть ref возвращена, если её безопасный контекст ссылки является вызывающим методом. Если его безопасный контекст ссылки является текущим методом или блоком, возвращение запрещено. В следующем фрагменте кода показаны два примера. Поле-член можно получить из области вызова метода, поэтому безопасный контекст поля ссылок класса или структуры является вызывающим методом. Контекст безопасной ссылки для параметра с ref или in модификаторами — это весь метод. Оба могут быть ref возвращены из метода-члена:

private int anIndex;

public ref int RetrieveIndexRef()
{
    return ref anIndex;
}

public ref int RefMin(ref int left, ref int right)
{
    if (left < right)
        return ref left;
    else
        return ref right;
}

Замечание

Если модификатор ref readonly или in применяется к параметру, этот параметр может быть возвращен через ref readonly, а не ref.

Компилятор гарантирует, что ссылка не может избежать безопасного контекста ссылки. Вы можете безопасно использовать ref параметры, ref return, и ref локальные переменные, так как компилятор обнаруживает, если вы случайно написали код, в котором можно получить доступ к выражению ref, когда его хранилище недействительно.

Безопасные структуры контекста и ссылок

ref struct типы требуют больше правил, чтобы обеспечить их безопасное использование. Тип ref struct может включать ref поля. Для этого требуется введение безопасного контекста. Для большинства типов безопасный контекст является вызывающим методом. Другими словами, значение, которое не ref struct всегда может быть возвращено из метода.

Неформальный безопасный контекст для ref struct определяется областью, в которой ко всем его ref полям можно получить доступ. Другими словами, это пересечение безопасного контекста ссылок всех его ref полей. Следующий метод возвращает ReadOnlySpan<char> к члену поля, так что его безопасный контекст — это сам метод.

private string longMessage = "This is a long message";

public ReadOnlySpan<char> Safe()
{
    var span = longMessage.AsSpan();
    return span;
}

В отличие от этого, следующий код выдает ошибку, так как ref field член Span<int> ссылается на массив целых чисел, выделенный в стеке. Он не может избежать метода

public Span<int> M()
{
    int length = 3;
    Span<int> numbers = stackalloc int[length];
    for (var i = 0; i < length; i++)
    {
        numbers[i] = i;
    }
    return numbers; // Error! numbers can't escape this method.
}

Объединение типов памяти

Введение System.Span<T> и System.Memory<T> предоставляют единую модель для работы с памятью. System.ReadOnlySpan<T> и System.ReadOnlyMemory<T> предоставьте доступные для чтения версии для доступа к памяти. Все они предоставляют абстракцию по блоку памяти, в котором хранится массив похожих элементов. Разница заключается в том, что Span<T> и ReadOnlySpan<T> являются типами ref struct, тогда как Memory<T> и ReadOnlyMemory<T> являются типами struct. Диапазоны содержат ref field. Поэтому экземпляры цепочки не могут покинуть свой безопасный контекст. Безопасный контекст объекта ref struct — это безопасный контекст ссылки его ref field. Реализация Memory<T> и ReadOnlyMemory<T> устраняет это ограничение. Эти типы используются для прямого доступа к буферам памяти.

Повышение производительности с помощью безопасности указателей

Использование этих функций для повышения производительности включает следующие задачи:

  • Избегайте распределения ресурсов: При изменении типа данных с class на struct, вы изменяете способ его хранения. Локальные переменные хранятся в стеке. Члены хранятся встроенно, когда выделяется объект контейнера. Это изменение означает меньше выделений и уменьшает работу сборщика мусора. Это также может снизить давление памяти, поэтому сборщик мусора работает реже.
  • Сохранение ссылочной семантики: изменение типа из class в struct изменяет семантику передачи переменной в метод. Код, изменяющий состояние параметров, нуждается в изменении. Теперь, когда параметр является параметром struct, метод изменяет копию исходного объекта. Вы можете восстановить исходную семантику, передав этот параметр в ref качестве параметра. После этого изменения метод изменяет исходный файл struct еще раз.
  • Избегайте копирования данных: копирование больших struct типов может повлиять на производительность в некоторых путях кода. Вы также можете добавить модификатор ref, чтобы передавать более крупные структуры данных методам по ссылке, а не по значению.
  • Ограничение изменений: когда struct тип передается по ссылке, вызываемый метод может изменить состояние структуры. Вы можете заменить модификатор ref на модификаторы ref readonly или in, чтобы указать, что аргумент нельзя изменить. Предпочитайте ref readonly, когда метод захватывает параметр или возвращает его по ссылке только для чтения. Вы также можете создавать readonly struct типы или struct типы с readonly элементами, чтобы обеспечить более широкий контроль над тем, какие элементы struct можно изменить.
  • Непосредственное управление памятью: некоторые алгоритмы наиболее эффективны при обработке структур данных в виде блока памяти, содержащего последовательность элементов. Типы Span и Memory обеспечивают безопасный доступ к блокам памяти.

Ни один из этих методов не требует кода unsafe . При грамотном использовании вы можете получить характеристики производительности из безопасного кода, которые ранее были возможны только с помощью небезопасных методов. Вы можете попробовать методы, описанные в руководстве по сокращению выделения памяти.