Хабрахабр

[Перевод] Struct и readonly: как избежать падения производительности

Использование типа Struct и модификатора readonly иногда может порождать падения производительности. Сегодня мы расскажем о том, как этого избегать, используя один Open Source анализатор кода — ErrorProne.NET.

Оставив в стороне вопрос изменяемости, замечу, что поведение структур с модификатором readonly (только для чтения) и без него в контекстах readonly сильно различается. Как вы, вероятно, знаете из моих предыдущих публикаций «The 'in'-modifier and the readonly structs in C#» («Модификатор in и структуры readonly в C#») и «Performance traps of ref locals and ref returns in C#» («Ловушки производительности при использовании локальных переменных и возвращаемых значений с модификатором ref»), работать со структурами сложнее, чем может показаться.

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

Вот краткий перечень предостережений, которые вы должны помнить:

  • Использование больших структур, которые передаются или возвращаются по значению, может вызвать проблемы с производительностью на критичных путях выполнения программы.
  • x.Y вызывает создание защитной копии x, если:
    • x является полем readonly;
    • тип x — это структура без модификатора readonly;
    • Y — не поле.

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

И, что наиболее важно, код, который опирается на эти правила, очень хрупкий (т. Вот несколько правил, которые следует запомнить. изменения, вносимые в код, немедленно порождают существенные изменения в других частях кода или документации — прим. е. Сколько человек заметят, что замена public readonly int X; на public int X в часто используемой структуре без модификатора readonly существенно влияет на производительность? перев.). Это действительно возможно при использовании свойства in параметра в цикле, когда защитная копия создается при каждой итерации. Или насколько легко увидеть, что передача параметра с помощью модификатора in вместо передачи по значению может снизить производительность?

И зов был услышан. Такие свойства структур буквально взывают к разработке анализаторов. NET — набор анализаторов, который информирует вас о возможности изменения программного кода для улучшения его дизайна и производительности при работе со структурами. Встречайте ErrorProne.

Анализ кода с выводом сообщений «Сделайте структуру X readonly»

Лучший способ избежать трудноуловимых ошибок и негативного влияния на производительность при использовании структур — по возможности сделать их readonly. Модификатор readonly в объявлении структуры четко выражает намерение разработчика (подчеркивая, что структура неизменяема) и помогает компилятору избежать порождения защитных копий во многих контекстах, упомянутых выше.

Вы можете без опасений запустить фиксер (процесс исправления кода) в пакетном режиме и объявить все структуры всего программного решения доступными только для чтения. Объявление структуры readonly не нарушает целостности кода.

Дружественность по отношению к модификатору ref readonly

Следующий шаг — оценка безопасности использования новых возможностей (модификатора in, локальных переменных ref readonly и т. п.). Это означает, что компилятор не будет создавать скрытые защитные копии, способные снизить производительность.

Можно рассмотреть три категории типов:

  • структуры, дружественные к ref readonly, использование которых никогда не приводит к созданию защитных копий;
  • структуры, недружественные к ref readonly, использование которых в контексте readonly всегда приводит к созданию защитных копий;
  • нейтральные структуры — структуры, использование которых может порождать защитные копии в зависимости от члена, применяемого в контексте readonly.

Первая категория включает в себя структуры readonly и POCO-структуры. Компилятор никогда не породит защитную копию, если структура является readonly. Также безопасно в контексте readonly использовать POCO-структуры: доступ к полям считается безопасным, и защитные копии не создаются.

В этом случае любой доступ к открытому члену в контексте readonly вызовет создание защитной копии. Вторая категория — это структуры без модификатора readonly, не содержащие открытых полей.

В этом случае компилятор создает защитные копии в зависимости от используемого члена. Последняя категория — это структуры с полями public или internal и свойствами или методами public либо internal.

д. Такое разделение помогает мгновенно выводить предупреждения, если «недружественная» структура передается с модификатором in, сохраняется в локальной переменной ref readonly и т.

Модификаторы in и ref readonly разработаны с целью оптимизации, специально, чтобы избежать создания избыточных копий. Анализатор не выводит предупреждения, если «недружественная» структура используется как поле readonly, поскольку альтернатива в этом случае отсутствует. В этом отношении поля readonly ведут себя иначе: если вы хотите сделать тип неизменяемым, то должны использовать эти поля. Если структура «недружелюбна» по отношению к этим модификаторам, у вас есть другие возможности: передать аргумент по значению или сохранить копию в локальной переменной. Помните: код должен быть ясным и элегантным, и только во вторую очередь — быстрым.

Анализ скрытых копий

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

Анализатор выявляет следующие скрытые копии:

  1. Скрытая копия поля readonly.
  2. Скрытая копия аргумента in.
  3. Скрытая копия локальной переменной ref readonly.
  4. Скрытая копия возвращаемого значения ref readonly.
  5. Скрытая копия при вызове метода расширения, принимающего параметр с модификатором this по значению, для экземпляра структуры.

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».

Вот результаты:

  1. В проектах с высокими требованиями к производительности, как правило, содержится множество структур, и большинство из них можно сделать readonly. Например, в проекте Roslyn анализатор обнаружил около 400 структур, которые можно сделать readonly, и в проекте D — примерно 300.
  2. В проектах с высокими требованиями к производительности скрытые копии должны создаваться лишь в исключительных ситуациях. Я обнаружил лишь несколько таких случаев в проекте Roslyn, поскольку большая часть структур имеет поля public вместо свойств public. Это позволяет избежать создания защитных копий в ситуации, когда структуры сохраняются в полях readonly. Скрытых копий в проекте D было больше, поскольку, по крайней мере, половина из них имела свойства get-only (доступ только для чтения).
  3. Передача даже довольно больших структур с использованием модификатора in, скорее всего, очень слабо (практически незаметно) влияет на время сквозного прохождения программы.

Я изменил все 300 структур в проекте D, сделав их readonly, а затем откорректировал сотни случаев их использования, указав, что они передаются с модификатором in. Затем я измерил сквозное время прохождения для различных сценариев производительности. Различия были статистически незначительными.

Вовсе нет. Означает ли это, что описанные выше возможности бесполезны?

В самом деле, в ряде случаев структуры в нашем коде передавались с модификатором ref, и некоторые поля были объявлены без модификатора readonly, чтобы исключить порождение защитных копий. Работа над проектом с высокими требованиями к производительности (например, над Roslyn или «Проектом D») подразумевает, что большое количество людей тратят массу времени на различные виды оптимизации. Отсутствие роста производительности при передаче структур с модификатором in может означать, что код был хорошо оптимизирован и на критических путях его прохождения отсутствует избыточное копирование структур.

Что я должен делать с этими возможностями?

Я считаю, что вопрос использования модификатора readonly для структур не требует долгих размышлений. Если структура неизменяема, то модификатор readonly просто явно принуждает компилятор к такому проектному решению. И отсутствие защитных копий для подобных структур — просто бонус.

Сегодня мои рекомендации таковы: если структуру можно сделать readonly, то непременно сделайте ее таковой.

Использование других рассмотренных возможностей имеет нюансы.

Предварительная оптимизация против предварительной пессимизации?

Герб Саттер (Herb Sutter) в своей удивительной книге «Стандарты кодирования на C++: 101 правило, рекомендации и передовой опыт» вводит понятие «предварительной пессимизации».

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

Если вы знаете, что структура относительно велика (40 байт и более), то можете всегда передавать ее с модификатором in. С моей точки зрения, параметр с модификатором in — как раз тот самый случай. Цена использования модификатора in сравнительно невелика, поскольку при этом не нужно корректировать вызовы, а выгоду можно получить реальную.

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

Заключение

  1. Используйте модификатор readonly для структур там, где это возможно.
  2. Рассмотрите возможность использования модификатора in для больших структур.
  3. Рассмотрите возможность использования локальных переменных и возвращаемых значений с модификатором ref readonly для кодирования библиотек или в тех случаях, когда результаты профилирования кода говорят о том, что это может быть полезно.
  4. Пользуйтесь ErrorProne.NET для обнаружения проблем с кодом и делитесь результатами.
Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть