Написание High-Performance управляемых приложений: руководство
Грегор Норискин
Группа производительности Microsoft CLR
Июнь 2003 г.
Область применения:
Microsoft® платформа .NET Framework
Сводка: Узнайте о среде CLR платформа .NET Framework с точки зрения производительности. Узнайте, как определить рекомендации по производительности управляемого кода и как измерить производительность управляемого приложения. (19 печатных страниц)
Скачайте профилировщик CLR. (330 КБ)
Содержимое
Жонглирование как метафора для разработки программного обеспечения
Среда CLR .NET
Управляемые данные и сборщик мусора
Профили распределения
API профилирования и профилировщик CLR
Размещение сборки мусора сервера
Завершения
Шаблон удаления
Примечание о слабых ссылках
Управляемый код и JIT-код CLR
Типы значений
Обработка исключений.
Потоки и синхронизация
Отражение
Поздняя привязка
Безопасность
COM-взаимодействие и вызов платформы
Счетчики производительности
Другие инструменты
Заключение
Ресурсы
Жонглирование как метафора для разработки программного обеспечения
Жонглирование является отличной метафорой для описания процесса разработки программного обеспечения. Жонглирование обычно требует по крайней мере трех элементов, хотя верхнего ограничения на количество элементов, которые можно попробовать жонглировать, не существует. Когда вы начинаете учиться, как жонглировать вы обнаруживаете, что вы watch каждый мяч по отдельности, как вы ловите и бросаете их. По мере того как вы прогрессируете, вы начинаете фокусироваться на потоке шаров, в отличие от каждого отдельного мяча. Когда вы освоили жонглирование, вы можете еще раз сосредоточиться на одном мяче, балансируя этот шар на носу, продолжая жонглировать другими. Вы интуитивно знаете, где будут шары, и можете положить руку в нужном месте, чтобы поймать и бросить их. Так как это похоже на разработку программного обеспечения?
Различные роли в процессе разработки программного обеспечения жонглирует разными "троицами"; Руководители проектов и программ жонглировать функциями, ресурсами и временем, а разработчики программного обеспечения — правильностью, производительностью и безопасностью. Всегда можно попытаться жонглировать больше предметов, но, как любой студент жонглирования может засвидетельствовать, добавление одного мяча делает его экспоненциально труднее держать шары в воздухе. Технически, если вы жонглируете менее трех шаров вы не жонглируете на всех. Если как разработчик программного обеспечения вы не рассматриваете правильность, производительность и безопасность написанного кода, можно сделать так, что вы не выполняете свою работу. Когда вы начинаете рассматривать правильность, производительность и безопасность, вам придется сосредоточиться на одном аспекте за раз. По мере того как они станут частью вашей повседневной практики вы обнаружите, что вам не нужно сосредоточиться на определенном аспекте, они будут просто частью того, как вы работаете. Когда вы овладеете ими, вы сможете интуитивно сделать компромиссы, и сосредоточить свои усилия соответствующим образом. И, как и в случае с жонглированием, практика является ключом.
Написание высокопроизводительного кода имеет собственную троицу; Установка целей, измерение и понимание целевой платформы. Если вы не знаете, насколько быстро должен быть код, как вы узнаете, когда закончите? Если вы не измеряете и не профилируйте код, как вы узнаете, когда вы достигли своих целей или почему вы не достигли своих целей? Если вы не понимаете, на какую платформу вы ориентируетесь, как вы будете знать, что оптимизировать в случае, если вы не выполняете свои цели. Эти принципы применяются к разработке высокопроизводительного кода в целом, независимо от целевой платформы. Ни в каких статьях о написании высокопроизводительного кода не будет полной без упоминания этой троицы. Хотя все три одинаково важны, в этой статье основное внимание уделяется двум последним аспектам, так как они применяются к написанию высокопроизводительных приложений, предназначенных для платформа .NET Framework Майкрософт®.
Основные принципы написания высокопроизводительного кода на любой платформе:
- Установка целей производительности
- Измерение, измерение, а затем измерение еще
- Общие сведения об аппаратных и программных платформах, на которые нацелено ваше приложение
Среда CLR .NET
Ядром платформа .NET Framework является среда CLR. Среда CLR предоставляет все службы среды выполнения для кода; JIT-компиляция, управление памятью, безопасность и ряд других служб. Среда CLR была разработана для обеспечения высокой производительности. Тем не более что есть способы, которыми вы можете воспользоваться этой производительностью и способы, которые вы можете помешать ей.
Цель этой статьи — дать обзор среды CLR с точки зрения производительности, определить рекомендации по производительности управляемого кода и показать, как можно измерить производительность управляемого приложения. Эта статья не является исчерпывающим обсуждением характеристик производительности платформа .NET Framework. Для целей этой статьи я определю производительность, включая пропускную способность, масштабируемость, время запуска и использование памяти.
Управляемые данные и сборщик мусора
Одной из основных проблем разработчиков, связанных с использованием управляемого кода в критически важных для производительности приложениях, является стоимость управления памятью СРЕДЫ CLR, которая осуществляется сборщиком мусора (GC). Затраты на управление памятью — это функция затрат на выделение памяти, связанной с экземпляром типа, затраты на управление этой памятью в течение всего времени существования экземпляра и стоимость освобождения этой памяти, когда она больше не нужна.
Управляемое выделение обычно очень дешевое; в большинстве случаев занимает меньше времени, чем C/C++ malloc
или new
. Это связано с тем, что среде CLR не нужно сканировать свободный список, чтобы найти следующий доступный непрерывный блок памяти, достаточно большой для хранения нового объекта; он сохраняет указатель на следующую свободную позицию в памяти. Распределение управляемых кучи можно рассматривать как "стек". Выделение может привести к возникновению коллекции, если сборке мусора требуется освободить память для выделения нового объекта. В этом случае выделение обходится дороже, чем или malloc
new
. Закрепленные объекты также могут повлиять на затраты на выделение. Закрепленные объекты — это объекты, которые сборке мусора было поручено не перемещать во время коллекции, как правило, из-за того, что адрес объекта был передан в собственный API.
malloc
В отличие от или new
, управление памятью в течение времени существования объекта связано с затратами. Сборка мусора CLR является поколением, что означает, что вся куча не всегда собирается. Однако сборке мусора по-прежнему необходимо знать, есть ли какие-либо живые объекты в остальных корневых объектах кучи в той части кучи, которая собирается. Память, содержащая объекты, содержащие ссылки на объекты в молодых поколениях, требует больших затрат на управление в течение времени существования объектов.
Сборка мусора является сборщиком меток поколений и очистки мусора. Управляемая куча содержит три поколения; Поколение 0 содержит все новые объекты, поколение 1 — немного более долгоживущие объекты, а поколение 2 — долгоживущие объекты. Сборка мусора соберет наименьшую часть кучи, чтобы освободить достаточно памяти для продолжения работы приложения. Коллекция Поколения включает в себя коллекцию всех молодых поколений, в данном случае коллекция Поколения 1 также собирает поколение 0. Поколение 0 имеет динамический размер в соответствии с размером кэша процессора и скоростью выделения приложения и обычно занимает менее 10 миллисекунд. Поколение 1 имеет динамический размер в соответствии со скоростью выделения в приложении и обычно занимает от 10 до 30 миллисекунд для сбора. Размер поколения 2 будет зависеть от профиля выделения приложения, а также времени, необходимого для сбора данных. Именно эти коллекции поколения 2 в наибольшей степени влияют на затраты на производительность управления памятью приложений.
ПОДСКАЗКА Сборка мусора выполняется самостоятельно и настраивается в соответствии с требованиями приложений к памяти. В большинстве случаев программный вызов сборки мусора будет препятствовать этой настройке. "Помощь" сборке мусора путем вызова GC. Сбор данных, скорее всего, не повысит производительность приложений.
Сборка мусора может перемещать динамические объекты во время коллекции. Если эти объекты большие, стоимость перемещения высока, поэтому эти объекты выделяются в специальной области кучи, которая называется кучей больших объектов. Куча больших объектов собирается, но не сжимается, например, крупные объекты не перемещаются. Крупные объекты — это объекты, размер которых превышает 80 КБ. Обратите внимание, что в будущих версиях среды CLR это может измениться. Когда требуется собрать кучу больших объектов, она принудительно создает полную коллекцию, а куча больших объектов собирается во время коллекций 2-го поколения. Распределение и коэффициент смертности объектов в куче больших объектов может существенно повлиять на производительность управления памятью приложений.
Профили распределения
Общий профиль выделения управляемого приложения определяет, насколько сложной должна быть работа сборщика мусора для управления памятью, связанной с приложением. Чем труднее сборке мусора приходится управлять памятью, тем большее количество циклов ЦП занимает сборка мусора, и тем меньше времени ЦП будет тратить на выполнение кода приложения. Профиль выделения — это функция количества выделенных объектов, размера этих объектов и времени их существования. Самый очевидный способ уменьшить нагрузку на сборку мусора — просто выделить меньше объектов. Приложения, предназначенные для расширяемости, модульности и повторного использования с использованием методов проектирования, ориентированных на объекты, почти всегда приводят к увеличению количества выделений. Существует штраф производительности за абстракцию и "элегантность".
В профиле выделения, понятном для сборки мусора, будут выделены некоторые объекты, выделенные в начале приложения, а затем выживаемые в течение всего времени существования приложения, а затем все остальные объекты будут кратковременными. Долгоживущие объекты будут содержать мало ссылок на кратковременные объекты или вообще не содержать их. Так как профиль выделения отклоняется от этого, сборке мусора придется больше работать, чтобы управлять памятью приложений.
Недружественный профиль выделения мусора будет иметь много объектов, сохранившихся в поколении 2 и затем умирающих, или будет иметь много кратковременных объектов, выделенных в куче больших объектов. Объекты, которые выживают достаточно долго, чтобы попасть в поколение 2, а затем умереть, являются самыми дорогими в управлении. Как упоминалось ранее, объекты в более старших поколениях, содержащие ссылки на объекты в молодом поколении во время сборки мусора, также увеличивают стоимость коллекции.
Типичный реальный профиль распределения будет находиться где-то между двумя профилями выделения, упомянутыми выше. Важной метрикой профиля выделения является процент от общего времени ЦП, затрачиваемого на сборку мусора. Это число можно получить из счетчика производительности сборки мусора в памяти среды CLR .NET: % времени в сборке мусора . Если среднее значение этого счетчика превышает 30 %, скорее всего, следует рассмотреть возможность более подробного рассмотрения профиля выделения. Это не обязательно означает, что ваш профиль выделения является "плохим"; Существуют приложения с большим объемом памяти, в которых этот уровень сборки мусора является необходимым и подходящим. Этот счетчик должен быть первым, на что вы посмотрите, если у вас возникли проблемы с производительностью; Он должен немедленно показать, является ли ваш профиль выделения частью проблемы.
ПОДСКАЗКА Если счетчик производительности .NET CLR Memory: % Time in GC указывает, что приложение тратит в среднем более 30 % времени на сборку мусора, следует более подробно изучить профиль выделения.
ПОДСКАЗКА Приложение, удобное для сборки мусора, будет иметь значительно больше коллекций поколения 0, чем поколение 2. Это соотношение можно установить, сравнив счетчики производительности NET CLR Memory: # Gen 0 и NET CLR Memory: # Gen 2 Collections.
API профилирования и профилировщик CLR
Среда CLR включает мощный API профилирования, который позволяет сторонним организациям создавать пользовательские профилировщики для управляемых приложений. Профилировщик CLR — это неподдерживаемый пример средства профилирования выделения, написанный группой разработчиков среды CLR, который использует этот API профилирования. Профилировщик CLR позволяет разработчикам просматривать профиль выделения для управляемых приложений.
Рис. 1. Главное окно профилировщика CLR
Профилировщик CLR включает в себя ряд очень полезных представлений профиля выделения, включая гистограмму выделенных типов, графики выделения и вызовов, временную линию, показывающую GCs разных поколений и итоговое состояние управляемой кучи после этих коллекций, а также дерево вызовов, показывающее выделение и загрузку сборок по методу.
Рис. 2. Граф выделения профилировщика CLR
ПОДСКАЗКА Дополнительные сведения об использовании профилировщика CLR см. в файле сведений, включенном в ZIP-файл.
Обратите внимание, что профилировщик CLR имеет высокую производительность и значительно изменяет характеристики производительности приложения. Возникающие ошибки стресса, вероятно, исчезнут при запуске приложения с помощью профилировщика CLR.
Размещение сборки мусора сервера
Для среды CLR доступны два разных сборщика мусора: сборка мусора рабочей станции и сборка мусора сервера. Консольные и Windows Forms приложения размещают сборку мусора рабочей станции, а ASP.NET — сборку мусора сервера. Сборка мусора сервера оптимизирована для пропускной способности и многопроцессорной масштабируемости. Сборка мусора сервера приостанавливает все потоки с управляемым кодом на весь период коллекции, включая этапы Mark и Sweep, а сборка мусора выполняется параллельно на всех ЦП, доступных процессу в выделенных высокоприоритетных потоках, сопоставленных с ЦП. Если потоки выполняют машинный код во время сборки мусора, эти потоки приостанавливаются только при возврате собственного вызова. При создании серверного приложения, которое будет выполняться на многопроцессорных компьютерах, настоятельно рекомендуется использовать сборку мусора сервера. Если приложение не размещено ASP.NET, вам придется написать собственное приложение, в котором явно размещается среда CLR.
ПОДСКАЗКА Если вы создаете масштабируемые серверные приложения, разместите сборку мусора сервера. См. раздел Реализация пользовательского узла среды CLR для управляемого приложения.
Сборка мусора рабочей станции оптимизирована для низкой задержки, которая обычно требуется для клиентских приложений. Не требуется заметной приостановки в клиентском приложении во время сборки мусора, так как обычно производительность клиента измеряется не по необработанной пропускной способности, а по воспринимаемой производительности. Сборка мусора рабочей станции выполняет параллельную сборку мусора, что означает, что она выполняет этап пометки во время выполнения управляемого кода. Сборка мусора приостанавливает потоки, выполняющие управляемый код, только когда необходимо выполнить этап очистки. В сборке мусора рабочей станции сборка мусора выполняется только в одном потоке и, следовательно, только на одном ЦП.
Завершения
Среда CLR предоставляет механизм, с помощью которого очистка выполняется автоматически перед освобождением памяти, связанной с экземпляром типа. Этот механизм называется завершением. Обычно завершение используется для освобождения собственных ресурсов, в данном случае подключения к базе данных дескрипторов операционной системы, которые используются объектом .
Завершение является дорогостоящей функцией и увеличивает давление, которое оказывается на сборку мусора. Сборка мусора отслеживает объекты, требующие завершения, в очереди, допускающей завершение. Если во время коллекции сборка мусора находит объект, который больше не является живым, но требует завершения, то запись этого объекта в очереди завершения перемещается в очередь FReachable. Завершение происходит в отдельном потоке, который называется потоком метода завершения. Поскольку во время выполнения метода завершения может потребоваться полное состояние объекта, объект и все объекты, на которые он указывает, будут повышены до следующего поколения. Память, связанная с объектом или графом объектов, освобождается только во время следующего сборки мусора.
Ресурсы, которые необходимо освободить, должны быть заключены в как можно меньше объекта Finalizable; Например, если классу требуются ссылки как на управляемые, так и на неуправляемые ресурсы, следует заключить неуправляемые ресурсы в новый класс Finalizable и сделать этот класс членом класса. Родительский класс не должен быть finalizable. Это означает, что только класс, содержащий неуправляемые ресурсы, будет повышен (при условии, что вы не храните ссылку на родительский класс в классе, содержавом неуправляемые ресурсы). Еще одна вещь, которую следует помнить, заключается в том, что существует только один поток завершения. Если метод завершения приводит к блокировке этого потока, последующие методы завершения не будут вызываться, ресурсы не будут освобождены, и ваше приложение будет иметь утечку.
ПОДСКАЗКА Методы завершения должны быть максимально простыми и никогда не должны блокироваться.
ПОДСКАЗКА Сделать завершаемым только класс-оболочку вокруг неуправляемых объектов, требующих очистки.
Завершение можно рассматривать как альтернативу подсчету ссылок. Объект, реализующий подсчет ссылок, отслеживает количество других объектов, имеющих ссылки на него (что может привести к некоторым хорошо известным проблемам), чтобы он смог освободить свои ресурсы, когда количество ссылок равно нулю. Среда CLR не реализует подсчет ссылок, поэтому ей необходимо предоставить механизм для автоматического освобождения ресурсов, когда больше нет ссылок на объект. Завершение — это механизм. Завершение обычно требуется только в том случае, если время существования объекта, требующего очистки, не известно явным образом.
Шаблон удаления
В случае, если время существования объекта известно явно, неуправляемые ресурсы, связанные с объектом, должны быть освобождены. Это называется "Удаление" объекта . Шаблон dispose реализуется через интерфейс IDisposable (хотя реализовать его самостоятельно было бы тривиальным). Если вы хотите сделать возможное завершение для класса, например сделать экземпляры класса удаляемыми, необходимо, чтобы объект реализовал интерфейс IDisposable и предоставил реализацию метода Dispose . В методе Dispose вы вызовете тот же код очистки, который находится в методе завершения, и сообщите сборке мусора, что ему больше не нужно завершать объект путем вызова сборки мусора. Метод SuppressFinalization . Рекомендуется, чтобы метод Dispose и метод завершения вызывали общую функцию завершения, чтобы поддерживать только одну версию кода очистки. Кроме того, если семантика объекта такова, что метод Close будет более логичным, чем метод Dispose , то следует также реализовать Close ; в этом случае подключение к базе данных или сокет логически "закрыты". Close может просто вызвать метод Dispose.
Всегда рекомендуется предоставлять метод Dispose для классов с методом завершения. никогда не может быть уверен, как будет использоваться этот класс, например, будет ли его время существования явным образом известно или нет. Если класс, который вы используете, реализует шаблон Dispose и вы явно знаете, когда вы закончите работу с объектом, наиболее определенно вызовите Dispose.
ПОДСКАЗКА Укажите метод Dispose для всех классов, которые можно завершить.
ПОДСКАЗКА Подавление завершения в методе Dispose .
ПОДСКАЗКА Вызовите общую функцию очистки.
ПОДСКАЗКА Если объект, который вы используете, реализует IDisposable и вы знаете, что объект больше не нужен, вызовите Dispose.
C# предоставляет очень удобный способ автоматического удаления объектов. Ключевое слово using
позволяет определить блок кода, после которого dispose будет вызываться для ряда удаленных объектов.
C# использует ключевое слово
using(DisposableType T)
{
//Do some work with T
}
//T.Dispose() is called automatically
Примечание о слабых ссылках
Любая ссылка на объект, который находится в стеке, в регистре, в другом объекте или в одном из других корней сборки мусора, будет поддерживать объект в активном состоянии во время сборки мусора. Как правило, это очень хорошо, учитывая, что обычно это означает, что приложение не выполняется с этим объектом. Однако бывают случаи, когда требуется иметь ссылку на объект, но не нужно влиять на его время существования. В таких случаях среда CLR предоставляет механизм, называемый слабыми ссылками, для этого. Любая строговая ссылка, например ссылка, которая является корнем объекта, может быть превращена в слабую ссылку. Примером использования слабых ссылок является создание объекта внешнего курсора, который может проходить через структуру данных, но не должен влиять на время существования объекта. Другой пример: если вы хотите создать кэш, который сбрасывается при нехватке памяти; например, когда происходит сборка мусора.
Создание слабой ссылки в C#
MyRefType mrt = new MyRefType();
//...
//Create weak reference
WeakReference wr = new WeakReference(mrt);
mrt = null; //object is no longer rooted
//...
//Has object been collected?
if(wr.IsAlive)
{
//Get a strong reference to the object
mrt = wr.Target;
//object is rooted and can be used again
}
else
{
//recreate the object
mrt = new MyRefType();
}
Управляемый код и JIT-код CLR
Управляемые сборки, которые являются единицей распространения для управляемого кода, содержат независимый от процессора язык, называемый промежуточным языком Майкрософт (MSIL или IL). JIT-среда CLR компилирует IL в оптимизированные собственные инструкции X86. JIT-компилятор является оптимизируемым компилятором, но так как компиляция происходит во время выполнения и только при первом вызове метода, количество оптимизаций, которое он выполняет, должно быть сбалансировано с временем, затраченным на компиляцию. Обычно это не является критическим для серверных приложений, так как время запуска и скорость реагирования обычно не являются проблемой, но это критически важно для клиентских приложений. Обратите внимание, что время запуска можно улучшить, выполнив компиляцию во время установки с помощью NGEN.exe.
Многие оптимизации, выполняемые JIT-файлом, не имеют связанных с ними программных шаблонов, например, вы не можете явно кодить для них, но есть число, которое делает. В следующем разделе рассматриваются некоторые из этих оптимизаций.
ПОДСКАЗКА Улучшайте время запуска клиентских приложений, скомпилируя приложение во время установки с помощью служебной программы NGEN.exe.
Встраивание метода
Вызовы методов связаны с затратами; аргументы должны быть отправлены в стек или сохранены в регистрах, необходимо выполнить пролог метода и эпилог и т. д. Затраты на эти вызовы можно избежать для некоторых методов, просто переместив тело метода вызываемого метода в тело вызывающего объекта. Это называется встраиванием метода. JIT использует ряд эвристических методов, чтобы решить, следует ли использовать встроенный метод. Ниже приведен список наиболее важных из них (обратите внимание, что это не является исчерпывающим):
- Методы, превышающие 32 байта IL, не будут встраиваться.
- Виртуальные функции не встраиваются.
- Методы со сложным управлением потоком не будут встроенными. Сложное управление потоком — это любое управление потоком, отличное от
if/then/else;
в данном случае илиswitch
while
. - Методы, содержащие блоки обработки исключений, не встраиваются, хотя методы, создающие исключения, по-прежнему являются кандидатами для встраивание.
- Если какой-либо из формальных аргументов метода является структурой, метод не будет встраивается.
Я хотел бы тщательно рассмотреть возможность явного программирования для этих эвристических средств, так как они могут измениться в будущих версиях JIT. Не скомпрометировать правильность метода, чтобы попытаться гарантировать, что он будет встраиваться. Интересно отметить, что ключевые inline
слова и __inline
в C++ не гарантируют, что компилятор встроит метод (хотя __forceinline
и ).
Методы property get и set, как правило, являются хорошими кандидатами для встраивание, так как все, что они делают, — это инициализация членов частных данных.
**ПОДСКАЗКА **Не скомпрометировать правильность метода в попытке гарантировать встраивание.
Исключение проверки диапазона
Одним из многих преимуществ управляемого кода является автоматическая проверка диапазона; каждый раз при доступе к массиву с помощью семантики array[index] JIT выдает проверка, чтобы убедиться, что индекс находится в границах массива. В контексте циклов с большим количеством итераций и небольшим количеством инструкций, выполняемых для каждой итерации, эти проверки диапазона могут быть дорогостоящими. Бывают случаи, когда JIT обнаруживает, что эти проверки диапазона являются ненужными, и устраняет проверка из основного текста цикла, проверяя его только один раз перед началом выполнения цикла. В C# существует программный шаблон, гарантирующий, что эти проверки диапазона будут устранены: явным образом проверить длину массива в инструкции "for". Обратите внимание, что незначительные отклонения от этого шаблона приведут к тому, что проверка не будет устранен и в этом случае добавит значение в индекс.
Исключение проверки диапазона в C#
//Range check will be eliminated
for(int i = 0; i < myArray.Length; i++)
{
Console.WriteLine(myArray[i].ToString());
}
//Range check will NOT be eliminated
for(int i = 0; i < myArray.Length + y; i++)
{
Console.WriteLine(myArray[i+x].ToString());
}
Оптимизация особенно заметна при поиске в больших массивах с массивами, например, когда исключены проверка диапазона внутреннего и внешнего цикла.
Оптимизации, требующие отслеживания использования переменных
Aa ряд оптимизаций JIT-компилятора требует, чтобы JIT отслеживал использование формальных аргументов и локальных переменных; например, когда они впервые используются и в последний раз они используются в тексте метода. В версиях 1.0 и 1.1 среды CLR существует ограничение 64 на общее количество переменных, для которых JIT будет отслеживать использование. Примером оптимизации, требующей отслеживания использования, является enregistration. Enregistration — это когда переменные хранятся в регистрах процессора, а не в кадре стека, например в ОЗУ. Доступ к зарегистрированным переменным выполняется значительно быстрее, чем в кадре стека, даже если переменная в кадре находится в кэше процессора. Для регистрации будет учитываться только 64 переменных; все остальные переменные будут отправлены в стек. Существуют и другие оптимизации, отличные от enregistration, которые зависят от отслеживания использования. Число формальных аргументов и локальных значений для метода должно быть меньше 64, чтобы обеспечить максимальное количество JIT-оптимизаций. Помните, что это число может измениться для будущих версий среды CLR.
ПОДСКАЗКА Оставьте методы короткими. Существует ряд причин этого, включая встраивание метода, регистрацию и длительность JIT.
Другие JIT-оптимизации
JIT-компилятор выполняет ряд других оптимизаций: распространение констант и копирования, циклическое инвариантное поднятие и ряд других. Нет явных шаблонов программирования, которые необходимо использовать для получения этих оптимизаций; они свободны.
Почему эти оптимизации не отображаются в Visual Studio?
Если вы используете пуск в меню Отладка или нажмите клавишу F5, чтобы запустить приложение в Visual Studio независимо от того, создали ли вы версию выпуска или отладку, все JIT-оптимизации будут отключены. Когда управляемое приложение запускается отладчиком, даже если оно не является отладочной сборкой приложения, JIT выдает неоптимизированные инструкции x86. Если вы хотите, чтобы JIT-код был оптимизирован, запустите приложение из Обозреватель Windows или нажмите клавиши CTRL+F5 из Visual Studio. Если вы хотите просмотреть оптимизированный дизассемблат и сравнить его с неоптимизованным кодом, можно использовать cordbg.exe.
ПОДСКАЗКА Используйте cordbg.exe для просмотра дизассемблированного и неоптимизированного кода, созданного JIT-кодом. После запуска приложения с cordbg.exe можно задать JIT-режим, введя следующее:
(cordbg) mode JitOptimizations 1
JIT's will produce optimized code
(cordbg) mode JitOptimizations 0
JIT-код создаст отлаживаемый (неоптимизированный) код.
Типы значений
Среда CLR предоставляет два разных набора типов: ссылочные типы и типы значений. Ссылочные типы всегда выделяются в управляемой куче и передаются по ссылке (как следует из имени). Типы значений выделяются в стеке или встроенные как часть объекта в куче и передаются по значению по умолчанию, хотя их также можно передать по ссылке. Типы значений очень дешевы в выделении, и, при условии, что они хранятся маленькими и простыми, они дешево передаются в качестве аргументов. Хорошим примером подходящего использования типов значений может быть тип значения Point, содержащий координаты x и y .
Тип значения точки
struct Point
{
public int x;
public int y;
//
}
Типы значений также можно рассматривать как объекты; например, для них можно вызывать методы объектов, их можно привести к объекту или передать там, где ожидается объект. Однако в этом случае тип значения преобразуется в ссылочный тип с помощью процесса Boxing. Если тип значения имеет тип Boxed, в управляемой куче выделяется новый объект, а значение копируется в новый объект . Это дорогостоящая операция, и она может снизить или полностью снизить производительность, получаемую при использовании типов значений. Если тип Boxed неявно или явно приводится обратно к типу значения, он является распакованным.
Тип значения box/unbox
C#:
int BoxUnboxValueType()
{
int i = 10;
object o = (object)i; //i is Boxed
return (int)o + 3; //i is Unboxed
}
MSIL:
.method private hidebysig instance int32
BoxUnboxValueType() cil managed
{
// Code size 20 (0x14)
.maxstack 2
.locals init (int32 V_0,
object V_1)
IL_0000: ldc.i4.s 10
IL_0002: stloc.0
IL_0003: ldloc.0
IL_0004: box [mscorlib]System.Int32
IL_0009: stloc.1
IL_000a: ldloc.1
IL_000b: unbox [mscorlib]System.Int32
IL_0010: ldind.i4
IL_0011: ldc.i4.3
IL_0012: add
IL_0013: ret
} // end of method Class1::BoxUnboxValueType
При реализации пользовательских типов значений (структуры в C#) следует рассмотреть возможность переопределения метода ToString . Если не переопределить этот метод, вызовы ToString для типа значения приведут к тому, что тип будет упакован. Это также относится к другим методам, унаследованным от System.Object, в данном случае — Equals, хотя ToString , вероятно, является наиболее часто вызываемым методом. Если вы хотите узнать, является ли тип значения boxed и когда, можно найти box
инструкцию в MSIL с помощью служебной программы ildasm.exe (как показано в фрагменте кода выше).
Переопределение метода ToString() в C# для предотвращения боксирования
struct Point
{
public int x;
public int y;
//This will prevent type being boxed when ToString is called
public override string ToString()
{
return x.ToString() + "," + y.ToString();
}
}
Имейте в виду, что при создании коллекций( например, ArrayList с плавающей точкой) каждый элемент будет упаковывался при добавлении в коллекцию. Рекомендуется использовать массив или создать пользовательский класс коллекции для типа значения.
Неявное boxing при использовании классов коллекций в C#
ArrayList al = new ArrayList();
al.Add(42.0F); //Implicitly Boxed becuase Add() takes object
float f = (float)al[0]; //Unboxed
Обработка исключений.
Обычно в качестве управления потоком обычно используются условия ошибок. В этом случае при попытке программного добавления пользователя в экземпляр Active Directory можно просто попытаться добавить пользователя, и если возвращается E_ADS_OBJECT_EXISTS HRESULT, вы знаете, что они уже существуют в каталоге. Кроме того, можно выполнить поиск пользователя в каталоге, а затем добавить пользователя только в том случае, если поиск завершается ошибкой.
Такое использование ошибок для управления обычным потоком является анти-шаблоном производительности в контексте среды CLR. Обработка ошибок в среде CLR выполняется с помощью структурированной обработки исключений. Управляемые исключения стоят очень дешево, пока вы не вызовете их. В среде CLR при возникновении исключения требуется пошаговое руководство по стеку, чтобы найти соответствующий обработчик исключений для созданного исключения. Стек ходьбы является дорогостоящей операцией. Исключения следует использовать, как следует из их названия; в исключительных или непредвиденных обстоятельствах.
**HINT **Рекомендуется возвращать перечислимый результат для ожидаемых результатов, а не создавать исключение для методов, критически важных для производительности.
**ПОДСКАЗКА **Существует ряд счетчиков производительности исключений СРЕДЫ CLR .NET, которые сообщают, сколько исключений создается в приложении.
**HINT **Если вы используете VB.NET используйте исключения, а не
On Error Goto
; объект ошибки является ненужным.
Потоки и синхронизация
Среда CLR предоставляет широкие возможности потоков и синхронизации, включая возможность создания собственных потоков, пула потоков и различных примитивов синхронизации. Прежде чем использовать поддержку потоков в среде CLR, следует тщательно продумать использование потоков. Имейте в виду, что добавление потоков может на самом деле уменьшить пропускную способность, а не увеличить ее, и вы можете быть уверены, что это увеличит использование памяти. В серверных приложениях, которые будут выполняться на многопроцессорных компьютерах, добавление потоков может значительно повысить пропускную способность за счет параллелизации выполнения (хотя это зависит от того, сколько происходит состязание за блокировку, например сериализация выполнения), а в клиентских приложениях добавление потока для отображения активности и (или) хода выполнения может повысить воспринимаемую производительность (при небольших затратах на пропускную способность).
Если потоки в приложении не специализированы для конкретной задачи или с ними связано специальное состояние, следует рассмотреть возможность использования пула потоков. Если вы использовали пул потоков Win32 в прошлом, пул потоков среды CLR будет вам очень знаком. Для каждого управляемого процесса существует один экземпляр пула потоков. Пул потоков зависит от количества потоков, которые он создает, и настраивается в соответствии с нагрузкой на компьютер.
Потоки не могут обсуждаться без обсуждения синхронизации; Все преимущества пропускной способности, которые многопоточность может дать приложению, могут быть сведены на нет неправильно записанной логикой синхронизации. Степень детализации блокировок может значительно повлиять на общую пропускную способность приложения, как из-за затрат на создание блокировки и управление ими, так и из-за того, что блокировки могут потенциально сериализовать выполнение. Я использую пример добавления узла в дерево, чтобы проиллюстрировать этот момент. Например, если дерево будет общей структурой данных, доступ к нему требуется нескольким потокам во время выполнения приложения, и вам потребуется синхронизировать доступ к дереву. Вы можете заблокировать все дерево при добавлении узла. Это означает, что вам придется нести только расходы на создание одной блокировки, но другие потоки, пытающиеся получить доступ к дереву, скорее всего, заблокируются. Это был бы пример грубой блокировки. Кроме того, вы можете заблокировать каждый узел при переходе по дереву, что будет означать, что вам придется нести расходы на создание блокировки для каждого узла, но другие потоки не будут блокироваться, если они не попытаются получить доступ к конкретному узлу, который вы заблокировали. Это пример оштрафованной блокировки. Вероятно, более подходящей степенью детализации блокировки будет блокировка только вложенного дерева, с которым вы работаете. Обратите внимание, что в этом примере вы, вероятно, будете использовать общую блокировку (RWLock), так как несколько читателей должны иметь возможность получить доступ одновременно.
Самый простой и высокопроизводительный способ выполнения синхронизированных операций — использовать класс System.Threading.Interlocked. Класс Interlocked предоставляет ряд низкоуровневых атомарных операций: Increment, Decrement, Exchange и CompareExchange.
Использование класса System.Threading.Interlocked в C#
using System.Threading;
//...
public class MyClass
{
void MyClass() //Constructor
{
//Increment a global instance counter atomically
Interlocked.Increment(ref MyClassInstanceCounter);
}
~MyClass() //Finalizer
{
//Decrement a global instance counter atomically
Interlocked.Decrement(ref MyClassInstanceCounter);
//...
}
//...
}
Вероятно, наиболее часто используемым механизмом синхронизации является мониторинг или критически важный раздел. Блокировку монитора можно использовать напрямую или с помощью lock
ключевое слово в C#. Ключевое слово lock
синхронизирует доступ для заданного объекта к определенному блоку кода. Блокировка монитора, которая довольно легко оспаривается, относительно дешева с точки зрения производительности, но становится более дорогой, если она сильно оспаривается.
Ключевое слово блокировки C#
//Thread will attempt to obtain the lock
//and block until it does
lock(mySharedObject)
{
//A thread will only be able to execute the code
//within this block if it holds the lock
}//Thread releases the lock
RWLock предоставляет общий механизм блокировки: например, "читатели" могут поделиться блокировкой с другими "читателями", но "модуль записи" не может. В тех случаях, когда это применимо, RWLock может привести к более высокой пропускной способности, чем использование монитора, что позволит получить блокировку только одному средству чтения или записи. Пространство имен System.Threading также включает класс Mutex. Мьютекс — это примитив синхронизации, который позволяет выполнять синхронизацию между процессами. Имейте в виду, что это значительно дороже, чем критически важный раздел, и его следует использовать только в том случае, когда требуется синхронизация между процессами.
Отражение
Отражение — это механизм, предоставляемый средой CLR, который позволяет получать сведения о типах программным способом во время выполнения. Отражение в значительной степени зависит от метаданных, которые внедряются в управляемые сборки. Многие API отражения требуют поиска и анализа метаданных, что является дорогостоящими операциями.
API отражения можно сгруппировать в три сегмента производительности; сравнение типов, перечисление членов и вызов элемента. Каждый из этих контейнеров становится все более дорогим. Операции сравнения типов (в данном случае typeof в C#, GetType, isInstanceOfType и т. д.) являются самыми дешевыми из API отражения, хотя и не являются дешевыми. Перечисления членов позволяют программно проверять методы, свойства, поля, события, конструкторы и т. д. класса. Примером того, где они могут использоваться, является сценарий времени разработки, в данном случае перечисление свойств таможенных веб-элементов управления для браузера свойств в Visual Studio. Наиболее ресурсоемкими API отражения являются те, которые позволяют динамически вызывать члены класса или динамически создавать JIT-код и выполнять метод . Конечно, существуют сценарии с поздним связыванием, в которых требуется динамическая загрузка сборок, экземпляров типов и вызовов методов, но такая слабая связь требует явного компромисса производительности. Как правило, в путях кода с учетом производительности следует избегать API отражения. Обратите внимание, что, хотя отражение не используется напрямую, его может использовать API, который вы используете. Поэтому следует также учитывать транзитивное использование API отражения.
Поздняя привязка
Поздние вызовы являются примером функции, которая использует отражение под крышками. Визуальные Basic.NET и JScript.NET поддерживают поздние вызовы. Например, не нужно объявлять переменную перед ее использованием. Объекты с поздней привязкой на самом деле относятся к объекту типа , а отражение используется для преобразования объекта в правильный тип во время выполнения. Вызов с поздней привязкой на порядок медленнее, чем прямой вызов. Если вам не требуется поведение с поздней привязкой, следует избегать его использования в путях кода, критически важных для производительности.
ПОДСКАЗКА Если вы используете VB.NET и не требуется явное позднее связывание, вы можете сообщить компилятору запретить его, включив
Option Explicit On
иOption Strict On
в начало исходных файлов. Эти параметры заставляют объявлять и строго вводить переменные, а также отключают неявное приведение.
Безопасность
Безопасность является необходимой и неотъемлемой частью среды CLR, и с ней связаны затраты на производительность. Если код является полностью доверенным, а политика безопасности используется по умолчанию, безопасность должна оказывать незначительное влияние на пропускную способность и время запуска приложения. Частично доверенный код, например код из зоны Интернета или интрасети, или сужение набора предоставления myComputer приведет к увеличению затрат на производительность безопасности.
COM-взаимодействие и вызов платформы
COM-взаимодействие и вызов платформы предоставляют собственные API для управляемого кода почти прозрачным способом; Вызов большинства собственных API обычно не требует специального кода, хотя может потребоваться несколько щелчков мышью. Как и следовало ожидать, вызов машинного кода из управляемого кода сопряжен с затратами и наоборот. Существует два компонента этой стоимости: фиксированные затраты, связанные с выполнением переходов между собственным и управляемым кодом, и переменные затраты, связанные с любым маршалированием аргументов и возвращаемых значений, которые могут потребоваться. Фиксированный вклад в затраты для COM-взаимодействия и P/Invoke невелик: обычно менее 50 инструкций. Стоимость маршалинга в управляемые типы и из них будет зависеть от того, насколько отличаются представления по обе стороны границы. Типы, требующие значительного объема преобразования, будут дороже. Например, все строки в среде CLR являются строками Юникода. Если вы вызываете API Win32 через P/Invoke, который ожидает массив символов ANSI, каждый символ в строке должен быть сужен. Однако если управляемый целочисленный массив передается там, где ожидается собственный целочисленный массив, маршалирование не требуется.
Так как вызов машинного кода связан с затратами на производительность, необходимо убедиться, что затраты оправданы. Если вы собираетесь выполнить собственный вызов, убедитесь, что работа, которую выполняет собственный вызов, оправдывает затраты на производительность, связанные с выполнением вызова. Сохраняйте методы "фрагментами", а не "болтливыми". Хороший способ измерить стоимость собственного вызова — измерить производительность собственного метода, который не принимает аргументов и не имеет возвращаемого значения, а затем измерить производительность собственного метода, который требуется вызвать. Разница даст вам представление о стоимости маршалинга.
ПОДСКАЗКА Выполняйте "chunky" COM-взаимодействие и вызовы P/Invoke в отличие от "чатных" вызовов и убедитесь, что стоимость вызова оправдывается объемом работы, которую выполняет вызов.
Обратите внимание, что с управляемыми потоками не связаны модели потоков. При выполнении вызова COM-взаимодействия необходимо убедиться, что поток, в который будет выполняться вызов, инициализирован правильной моделью потоков COM. Обычно это делается с помощью MTAThreadAttribute и STAThreadAttribute (хотя это также можно сделать программным способом).
Счетчики производительности
Для среды CLR .NET предоставляется ряд счетчиков производительности Windows. Эти счетчики производительности должны быть инструментом разработчика при первой диагностике проблемы с производительностью или при попытке определить характеристики производительности управляемого приложения. Я уже упоминал несколько счетчиков, связанных с управлением памятью и исключениями. Существуют счетчики производительности почти для каждого аспекта среды CLR и платформа .NET Framework. Эти счетчики производительности всегда доступны и не являются инвазивными; они имеют низкие издержки и не изменяют характеристики производительности приложения.
Другие инструменты
Помимо счетчиков производительности и профилировщика CLR, вам потребуется использовать обычный профилировщик, чтобы определить, какие методы в приложении занимают больше всего времени и вызываются чаще всего. Это будут методы, которые вы оптимизируете в первую очередь. Доступно несколько коммерческих профилировщиков, поддерживающих управляемый код, в том числе DevPartner Studio Professional Edition 7.0 от Compuware и VTune™ Анализатор производительности 7.0 от Intel®. Compuware также создает бесплатный профилировщик для управляемого кода под названием DevPartner Profiler Community Edition.
Заключение
В этой статье только начинается изучение среды CLR и платформа .NET Framework с точки зрения производительности. Существует множество других аспектов архитектуры среды CLR и платформа .NET Framework, которые повлияют на производительность приложения. Лучшее руководство, которое я могу дать любому разработчику, заключается в том, чтобы не делать никаких предположений о производительности платформы, на которую нацелено ваше приложение, и об api-интерфейсах, которые вы используете. Измеряйте все!
Счастливого жонглирования.
Ресурсы
- Compuware DevPartner Studio Professional Edition 7.0.
- Intel.VTune Анализатор производительности 7.0.
- Compuware DevPartner Profiler Community Edition.
- Ян Грей, написание более быстрого кода: знание стоимости вещей, MSDN.
- Рико Мариани (Rico Mariani), основные сведения о сборщике мусора и рекомендации по производительности, MSDN.