- PVSM.RU - https://www.pvsm.ru -
Использование типа Struct и модификатора readonly иногда может порождать падения производительности. Сегодня мы расскажем о том, как этого избегать, используя один Open Source анализатор кода — ErrorProne.NET.
Как вы, вероятно, знаете из моих предыдущих публикаций «The 'in'-modifier and the readonly structs in C# [1]» («Модификатор in и структуры readonly в C#») и «Performance traps of ref locals and ref returns in C# [2]» («Ловушки производительности при использовании локальных переменных и возвращаемых значений с модификатором ref»), работать со структурами сложнее, чем может показаться. Оставив в стороне вопрос изменяемости, замечу, что поведение структур с модификатором readonly (только для чтения) и без него в контекстах readonly сильно различается.
Предполагается, что структуры используются при программировании сценариев, требующих высокой производительности, и для эффективной работы с ними вы должны кое-что знать о различных скрытых операциях, порождаемых компилятором для обеспечения неизменности структуры.
Вот краткий перечень предостережений, которые вы должны помнить:
x.Y
вызывает создание защитной копии x, если:
x
является полем readonly;x
— это структура без модификатора readonly;Y
— не поле.Те же правила работают, если x является параметром с модификатором in, локальной переменной с модификатором ref readonly или результатом вызова метода, который возвращает значение посредством ссылки readonly.
Вот несколько правил, которые следует запомнить. И, что наиболее важно, код, который опирается на эти правила, очень хрупкий (т. е. изменения, вносимые в код, немедленно порождают существенные изменения в других частях кода или документации — прим. перев.). Сколько человек заметят, что замена public readonly int X
; на public int X { get; }
в часто используемой структуре без модификатора readonly существенно влияет на производительность? Или насколько легко увидеть, что передача параметра с помощью модификатора in вместо передачи по значению может снизить производительность? Это действительно возможно при использовании свойства in параметра в цикле, когда защитная копия создается при каждой итерации.
Такие свойства структур буквально взывают к разработке анализаторов. И зов был услышан. Встречайте ErrorProne.NET [3] — набор анализаторов, который информирует вас о возможности изменения программного кода для улучшения его дизайна и производительности при работе со структурами.
Лучший способ избежать трудноуловимых ошибок и негативного влияния на производительность при использовании структур — по возможности сделать их readonly. Модификатор readonly в объявлении структуры четко выражает намерение разработчика (подчеркивая, что структура неизменяема) и помогает компилятору избежать порождения защитных копий во многих контекстах, упомянутых выше.
Объявление структуры readonly не нарушает целостности кода. Вы можете без опасений запустить фиксер (процесс исправления кода) в пакетном режиме и объявить все структуры всего программного решения доступными только для чтения.
Следующий шаг — оценка безопасности использования новых возможностей (модификатора in, локальных переменных ref readonly и т. п.). Это означает, что компилятор не будет создавать скрытые защитные копии, способные снизить производительность.
Можно рассмотреть три категории типов:
Первая категория включает в себя структуры readonly и POCO-структуры. Компилятор никогда не породит защитную копию, если структура является readonly. Также безопасно в контексте readonly использовать POCO-структуры: доступ к полям считается безопасным, и защитные копии не создаются.
Вторая категория — это структуры без модификатора readonly, не содержащие открытых полей. В этом случае любой доступ к открытому члену в контексте readonly вызовет создание защитной копии.
Последняя категория — это структуры с полями public или internal и свойствами или методами public либо internal. В этом случае компилятор создает защитные копии в зависимости от используемого члена.
Такое разделение помогает мгновенно выводить предупреждения, если «недружественная» структура передается с модификатором in, сохраняется в локальной переменной ref readonly и т. д.
Анализатор не выводит предупреждения, если «недружественная» структура используется как поле readonly, поскольку альтернатива в этом случае отсутствует. Модификаторы in и ref readonly разработаны с целью оптимизации, специально, чтобы избежать создания избыточных копий. Если структура «недружелюбна» по отношению к этим модификаторам, у вас есть другие возможности: передать аргумент по значению или сохранить копию в локальной переменной. В этом отношении поля readonly ведут себя иначе: если вы хотите сделать тип неизменяемым, то должны использовать эти поля. Помните: код должен быть ясным и элегантным, и только во вторую очередь — быстрым.
Компилятор выполняет много действий, скрытых от пользователя. Как было показано в предыдущей публикации [2], довольно сложно увидеть, когда происходит создание защитной копии.
Анализатор выявляет следующие скрытые копии:
public struct NonReadOnlyStruct
{
public readonly long PublicField;
public long PublicProperty { get; }
public void PublicMethod() { }
private static readonly NonReadOnlyStruct _ros;
public static void Samples(in NonReadOnlyStruct nrs)
{
// Ok. Public field access causes no hidden copies
var x = nrs.PublicField;
// Ok. No hidden copies.
x = _ros.PublicField;
// Hidden copy: Property access on 'in'-parameter
x = nrs.PublicProperty;
// Hidden copy: Method call on readonly field
_ros.PublicMethod();
ref readonly var local = ref nrs;
// Hidden copy: method call on ref readonly local
local.PublicMethod();
// Hidden copy: method call on ref readonly return
Local().PublicMethod();
ref readonly NonReadOnlyStruct Local() => ref _ros;
}
}
Обратите внимание, что анализаторы выводят диагностические сообщения, только если размер структуры ≥16 байт.
Передача больших структур по значению и, как результат, создание компилятором защитных копий существенно влияют на производительность. По крайней мере, это показывают результаты тестов производительности. Но как эти явления повлияют на реальные приложения в терминах времени сквозного прохождения?
Чтобы протестировать анализаторы с помощью реального кода, я использовал их для двух проектов: проекта Roslyn и внутреннего проекта, над которым я сейчас работаю в компании Microsoft (проект представляет собой самостоятельное компьютерное приложение с жесткими требованиями к производительности); назовем его для ясности «Проект D».
Вот результаты:
Я изменил все 300 структур в проекте D, сделав их readonly, а затем откорректировал сотни случаев их использования, указав, что они передаются с модификатором in. Затем я измерил сквозное время прохождения для различных сценариев производительности. Различия были статистически незначительными.
Означает ли это, что описанные выше возможности бесполезны? Вовсе нет.
Работа над проектом с высокими требованиями к производительности (например, над Roslyn или «Проектом D») подразумевает, что большое количество людей тратят массу времени на различные виды оптимизации. В самом деле, в ряде случаев структуры в нашем коде передавались с модификатором ref, и некоторые поля были объявлены без модификатора readonly, чтобы исключить порождение защитных копий. Отсутствие роста производительности при передаче структур с модификатором in может означать, что код был хорошо оптимизирован и на критических путях его прохождения отсутствует избыточное копирование структур.
Я считаю, что вопрос использования модификатора readonly для структур не требует долгих размышлений. Если структура неизменяема, то модификатор readonly просто явно принуждает компилятор к такому проектному решению. И отсутствие защитных копий для подобных структур — просто бонус.
Сегодня мои рекомендации таковы: если структуру можно сделать readonly, то непременно сделайте ее таковой.
Использование других рассмотренных возможностей имеет нюансы.
Герб Саттер (Herb Sutter) в своей удивительной книге «Стандарты кодирования на C++: 101 правило, рекомендации и передовой опыт [4]» вводит понятие «предварительной пессимизации».
«При прочих равных условиях, сложность кода и его удобочитаемость, некоторые эффективные шаблоны проектирования и идиомы кодирования должны естественным образом стекать с кончиков ваших пальцев. Такой код не сложнее в написании, чем его пессимизированные альтернативы. Вы не занимаетесь предварительной оптимизацией, а избегаете добровольной пессимизации».
С моей точки зрения, параметр с модификатором in — как раз тот самый случай. Если вы знаете, что структура относительно велика (40 байт и более), то можете всегда передавать ее с модификатором in. Цена использования модификатора in сравнительно невелика, поскольку при этом не нужно корректировать вызовы, а выгоду можно получить реальную.
Напротив, для локальных переменных и возвращаемых значений с модификатором readonly ref дело обстоит иначе. Я бы сказал, что эти возможности следует использовать при кодировании библиотек, а в коде приложения от них лучше отказаться (только если профилирование кода не выявит, что операция копирования реально является проблемой). Использование этих возможностей требует от вас дополнительных усилий, а читателю кода становится сложнее его понять.
Автор: Александр Гуреев
Источник [6]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/c-2/292832
Ссылки в тексте:
[1] The 'in'-modifier and the readonly structs in C#: https://blogs.msdn.microsoft.com/seteplia/2018/03/07/the-in-modifier-and-the-readonly-structs-in-c/
[2] Performance traps of ref locals and ref returns in C#: https://blogs.msdn.microsoft.com/seteplia/2018/04/11/performance-traps-of-ref-locals-and-ref-returns-in-c/
[3] ErrorProne.NET: https://github.com/SergeyTeplyakov/ErrorProne.NET
[4] Стандарты кодирования на C++: 101 правило, рекомендации и передовой опыт: https://www.amazon.com/Coding-Standards-Rules-Guidelines-Practices/dp/0321113586
[5] ErrorProne.NET: https://blogs.msdn.microsoft.com/seteplia/2018/05/03/avoiding-struct-and-readonly-reference-performance-pitfalls-with-errorprone-net/
[6] Источник: https://habr.com/post/423053/?utm_source=habrahabr&utm_medium=rss&utm_campaign=423053
Нажмите здесь для печати.