Поиск по сайту:

Что такое объединение объектов? Улучшение производительности памяти в C#


Выделение памяти в C# относительно затратно и является ключевым моментом оптимизации для любого критичного к производительности приложения. Объединение объектов в пул — это один из методов, который может помочь снизить нагрузку на приложения, интенсивно использующие память.

Как объединение объектов повышает производительность?

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

Объединение объектов – это метод, используемый для сокращения распределения памяти. Часто нет необходимости выделять память, но вам может не понадобиться выделять память так часто, как вы это делаете. Выделение объектов происходит медленно по нескольким причинам: базовая память выделяется в куче (что намного медленнее, чем выделение в стеке типа значения), а сложные объекты могут иметь конструкторы, требовательные к производительности. Кроме того, поскольку это память на основе кучи, сборщику мусора нужно будет очистить ее, что может снизить производительность, если вы запускаете ее слишком часто.

Например, предположим, что у вас есть цикл, который выполняется много раз и выделяет новый объект, такой как список, для каждого выполнения. Используется много памяти, и она не очищается до тех пор, пока все не завершится и не запустится сборка мусора. Следующий код будет выполняться 10 000 раз и оставит 10 000 бесхозных списков, выделенных в памяти в конце функции.

Когда сборщик мусора в конце концов запустится, ему будет очень трудно очистить весь этот мусор, что негативно скажется на производительности в ожидании завершения сборки мусора.

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

В этом примере повторно используемым подходом будет запуск new List перед циклом для выполнения первого выделения, а затем запуск .Clear или сброс данных для экономии места в памяти и созданный мусор. После того, как этот цикл завершится, в памяти останется только один список, что намного меньше, чем 10 000 из них.

Объединение объектов в основном является общей реализацией этой концепции. Это набор объектов, которые можно использовать повторно. Для них нет официального интерфейса, но в целом они имеют внутреннее хранилище данных и реализуют два метода: GetObject() и ReleaseObject().

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

Вам нужно убедиться, что вы освобождаете объект, прежде чем использовать его снова, потому что в большинстве пулов будет максимальное количество пустых объектов, которые они держат под рукой. Если вы попытаетесь получить 1000 объектов из пула, вы его высушите, и он начнет нормально распределять их по запросу, что противоречит цели пула.

В критических для производительности случаях, особенно при частой работе с большим количеством повторяющихся данных, объединение объектов в пул может значительно повысить производительность. Однако это не универсально, и во многих случаях вам не захочется объединять объекты в пул. Выделения с помощью new по-прежнему выполняются довольно быстро, поэтому, если вы не очень часто выделяете большие куски памяти или выделяете объекты с помощью конструкторов с высокой производительностью, часто лучше просто использовать new. вместо ненужного объединения, особенно с учетом того, что сам пул добавляет дополнительные накладные расходы и сложность в распределении. Выделение одного обычного объекта всегда будет быстрее, чем выделение объекта в одном пуле; разница в производительности возникает, когда вы выделяете много раз.

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

Если вы хотите использовать пулы объектов, Microsoft предлагает реализацию в Microsoft.Extensions.ObjectPool. Если вы хотите реализовать это самостоятельно, вы можете открыть исходный код, чтобы проверить, как это работает в DefaultObjectPool. Как правило, у вас будет свой пул объектов для каждого типа с максимальным количеством объектов, которые нужно хранить в пуле.