|
|
Вопросы производительности .NET Framework
Цикл статей
image001.gif
image002.gif
image003.gif
Вопросы производительности .NET Framework
Этот раздел включает обзор различных технологий, работающих в управляемом мире, и техническое описание того, как они влияют на производительность. Это касается работы сборщика мусора, JIT, remoting, типов значений, безопасности и т.д.
Обзор
Среда выполнения .NET представляет несколько передовых технологий, предназначенных для обеспечения безопасности, облегчения разработки и производительности. Для разработчика важно понимать каждую из этих технологий и эффективно применять их в коде. Передовые инструментальные средства, предоставляемые средой выполнения, облегчают построение надежных приложений, но заставить эти приложения «летать» является (и всегда являлось) задачей разработчика.
Сборка мусора
Основные сведения
Сборка мусора (GC) освобождает память, занятую объектами, которые больше не используются, тем самым освобождая программиста от распространенных и сложных в отладке ошибок. Общая схема жизненного цикла объекта, как для управляемого, так и для машинного кода, такая:
Foo a = new Foo(); // Allocate memory for the object and Initialize …a… // Use the object delete a; // Tear down the state of the object, clean up // and free the memory for that object
В неуправляемом коде вам надо проделывать все эти операции самостоятельно. Упущение этапов распределения или очистки памяти, может привести к совершенно непредсказуемому поведению, что будет очень трудно отладить, а если вы забудете освободить объекты, могут возникнуть утечки памяти. Последовательность распределения памяти в Общеязыковой среде выполнения (CLR) очень похожа на только что рассмотренную. Если мы добавим GC-специфичную информацию, мы получим нечто очень похожее:
Foo a = new Foo(); // Allocate memory for the object and Initialize …a… // Use the object (it is strongly reachable) a = null; // A becomes unreachable (out of scope, nulled, etc) // Eventually a collection occurs, and a's resources // are torn down and the memory is freed
До тех пор, пока объект может быть освобожден, в обоих мирах предпринимаются одни и те же шаги. В неуправляемом коде вы должны помнить о необходимости освобождения объекта по окончании работы с ним. В управляемом коде, как только объект больше не используется, GC может удалить его. Конечно же, если ваш ресурс требует особого внимания для освобождения (скажем, закрытие соединения), GC может понадобиться помощь, для того чтобы закрыть его правильно. До сих пор применяется такой же код, как вы писали ранее для очистки ресурса перед освобождением, в форме методов Dispose() и Finalize().
Если вы сохраняете указатель на ресурс, GC никак не может знать, собираетесь ли вы использовать его в будущем. Это означает то, что правила, используемые вами в неуправляемом коде для явного освобождения объектов, до сих пор применимы, но в основном обрабатывать все для вас будет GC. Вместо того, чтобы 100% времени посвящать управлению памятью, это займет у вас всего 5% времени.
Сборщик мусора CLR – это относящийся к определенному поколению, сборщик. Он следует некоторым принципам, которые позволяют достигать превосходной производительности. Во-первых, известно, что объекты с коротким временем жизни обычно небольшие, к ним часто обращаются. GC разделяет таблицу распределения на несколько подтаблиц, называемых поколениями, что позволяет максимально сократить время сборки мусора. Поколение 0 включает молодые, часто используемые объекты. Это поколение самое маленькое, для сборки мусора в нем требуется около 10 миллисекунд. GC может игнорировать другие поколения во время этой сборки мусора, таким образом обеспечивается намного большая производительность. Поколения 1 и 2 предназначены для больших и более старых объектов, и сборка мусора в них происходит не так часто. Когда происходит сборка мусора в поколении 1, также просматривается и поколение 0. Сборка мусора в поколении 2 – это полная сборка мусора, и только здесь GC проходит всю таблицу. Это также приводит к разумному использованию кэшей CPU, который может настроить подсистему памяти под конкретный процессор, на котором выполняется GC.
Когда происходит сборка мусора?
Когда сделано распределение времени, GC проверяет необходимость сборки мусора. Учитывается размер мусора, размер оставшейся памяти, размеры каждого поколения, а затем для принятия решения используется эвристическое правило. Во время сборки мусора скорость распределения объекта обычно такая же (или больше), чем в С или С++.
Что происходит, когда идет сборка мусора?
Давайте проследим шаг за шагом, что происходит во время сборки мусора. GC сохраняет список ссылок, которые указывают на кучу GC. Если объект существует, есть и ссылка на его местоположение в куче. Объекты в куче также могут указывать друг на друга. Эта таблица указателей и должна быть проверена GC для того, чтобы освободить память. Последовательность событий такова:
1. Управляемая куча содержит все свое распределенное пространство в непрерывном блоке, когда этот блок меньше, чем требуется, вызывается GC.
2. GC прослеживает каждый ссылку и все сопровождающие указатели, составляя список недоступных объектов.
3. Каждый недоступный объект считается подлежащим уничтожению и помечается для сборки.

Рисунок 1. Перед сборкой мусора: обратите внимание, что не все блоки доступны!
4. Удаление объектов из списка доступных делает большинство объектов подлежащими уничтожению. Однако некоторые ресурсы должны быть обработаны специально. При определении объекта у вас есть выбор использования метода Dispose() или метода Finalize() (или и того, и другого).
5. Заключительным этапом в сборке мусора является фаза уплотнения. Все используемые объекты перемещаются в непрерывный блок, все указатели и ссылки обновляются.
6. Уплотняя живущие объекты и обновляя начальный адрес свободного пространства, GC поддерживает непрерывность всего свободного пространства. Если для размещения объекта достаточно места, GC возвращает элемент управления программе. В противном случае он вызывает OutOfMemoryException.

Рисунок 2. После сборки мусора: блоки, к которым происходят обращения, уплотнены. Больше свободного пространства!
Удаление объекта
Некоторые объекты нуждаются в специальной обработке до того, как ресурс сможет быть возвращен. Примерами таких ресурсов являются файлы, сетевые соединения или соединения с базами данных. Если вы хотите закрыть эти ресурсы изящно, простого освобождения памяти в куче будет недостаточно. Чтобы осуществить очистку объекта, вы можете использовать или метод Dispose(), или метод Finalize(), или сразу оба этих метода.
Метод Finalize():
- Вызывается сборщиком мусора
- Не гарантируется, что будет вызван в каком-либо порядке или в предсказуемое время
- После вызова освобождает память после следующей сборки мусора
- Сохраняет все дочерние объекты до следующей сборки мусора
Метод Dispose():
- Вызывается программистом
- Размещается и планируется программистом
- Возвращает ресурсы по завершению метода
Управляемые объекты, которые удерживают только управляемые ресурсы, не нуждаются в этих методах. Ваша программа, вероятно, будет использовать только несколько комплексных ресурсов, и, возможно, вы будете знать, что они из себя представляют и когда они вам нужны. Если вам известно и то, и другое, нет причины надеяться на финализаторы, т.к. вы можете вручную произвести очистку. Есть несколько причин на то, чтобы вы захотели сделать так, и все они связаны с очередью финализатора.
В GC, когда объект, имеющий финализатор, отмечен как подлежащий уничтожению, он и любой объект, на который он указывает, помещаются в специальную очередь. Отдельный поток проходит по этой очереди, вызывая метод Finalize() каждого элемента очереди. Программист не контролирует этот поток или порядок расположения элементов в очереди. GC может вернуть управление программе, в то время как финализация объектов в очереди еще не будет проведена. Эти объекты могут остаться в памяти, сохраняемые в очереди длительное время. Вызовы на финализацию делаются автоматически, и сам вызов не оказывает прямого влияния на производительность. Однако недетерминированная модель финализации определенно может иметь другие непрямые последствия:
- В сценарии, в котором задействованы ресурсы, которые должны быть освобождены в определенное время, в случае использования финализаторов вы теряете контроль. Скажем, у вас есть открытый файл и из соображений безопасности его надо закрыть. Даже после того как вы обнулите объект и вызовите GC, файл останется открытым до тех пор, пока не будет вызван его метод Finalize(), и вы не имеете никакого представления, когда это может быть сделано.
- N объектов, которые должны быть освобождены в определенном порядке, могут быть обработаны неправильно.
- Огромный объект с дочерними объектами может занимать очень много памяти, требуя дополнительных сборок мусора и понижая производительность. Такие объекты могут оставаться в памяти длительное время.
- Маленький объект, который должен быть финализирован, может иметь указатели на большие ресурсы, которые могут быть освобождены в любое время. Эти объекты не будут освобождены до тех пор, пока на них ссылается объект, который должен быть финализирован, создавая ненужное сжатие памяти и вызывая частые сборки мусора.
Диаграмма состояния на рисунке 3 иллюстрирует различные пути, которые может пройти ваш объект в условиях финализации или освобождения:

Рисунок 3. Пути освобождения и финализации, которые может пройти объект
Как видите, финализация добавляет несколько шагов в жизненный цикл объекта. Если вы самостоятельно освобождаете объект, он может быть удален и память будет возвращена при следующей сборке мусора. Когда необходимо, чтобы произошла финализация, вам придется ожидать до тех пор, пока ни будет вызван существующий метод. Т.к. неизвестно, когда это произойдет, вы можете иметь большое количество связанной памяти и зависеть от очереди финализации. Это может быть крайне проблематичным, если ваш объект соединен со всем деревом объектов, и они все располагаются в памяти в ожидании финализации.
Как выбрать, какой сборщик мусора использовать?
В CLR есть два разных GC: Рабочая станция (mscorwks.dll) и Сервер (mscorsvr.dll). При работе в режиме Рабочей станции больший интерес, чем пространство и эффективность, представляет задержка . Сервер с множеством процессоров и клиентов, соединенных через сеть, может позволить некоторую задержку, но основной является производительность. Microsoft включила два сборщика мусора, каждый из которых приспособлен к определенной ситуации.
GC Сервер:
- Много процессорный масштабируемый
- Один поток GC на CPU
- Программа останавливается во время сборки
GC Рабочая станция:
- Минимизирует паузы путем одновременной работы во время полной сборки мусора
GC Сервер разработан для обеспечения максимальной пропускная способность, он масштабируется с очень высокой производительностью. Фрагментация памяти более проблематична в серверах, чем в рабочих станциях, что делает сборку мусора очень привлекательным предложением. В однопроцессорном сценарии оба сборщика мусора работают одинаково: режим рабочей станции без одновременной сборки мусора. В многопроцессорных машинах GC рабочая станция использует второй процессор для того, чтобы одновременно запустить сборку мусора, минимизируя запаздывание. GC сервер использует множество куч и потоков сборки мусора, чтобы обеспечить максимальное увеличение производительности и лучшую масштабируемость.
Вы можете выбирать, какой GC использовать. Когда вы загружаете среду выполнения в процесс, вы определяете, какой сборщик мусора использовать.
Миф: сборка мусора с помощью GC всегда медленнее, чем сборка мусора вручную
В действительности, GC работает намного быстрее, чем ручная сборка мусора в С. Это удивляет многих людей, поэтому требует некоторого объяснения. Прежде всего обратите внимание, что поиск свободного пространства идет постоянно. Т.к. все свободное пространство непрерывно, GC просто следует по указателю и проверяет, достаточно ли там места. В С вызов malloc() обычно приводит к поиску связного списка свободных блоков. Это может потребовать определенного времени, особенно если ваша куча сильно фрагментирована. Чтобы еще усугубить дело, некоторые реализации среды выполнения С блокируют кучу во время этой процедуры. Как только память распределена или использована, список должен быть обновлен. При использовании сборщика мусора распределение происходит свободно и память высвобождается во время сборки мусора.
Более опытные программисты будут резервировать большие блоки памяти и обрабатывать распределение в пределах этих блоков самостоятельно. Недостатком этого подхода является то, что фрагментация памяти станет огромной проблемой для программистов, и это вынудит их добавлять большое количество логики для обработки памяти в свои приложения. И наконец, сборщик мусора не добавляет непроизводительных издержек. Распределение происходит так же быстро или быстрее и уплотнение проводится автоматически, это позволяет программистам сосредоточиться на реализации приложений.
У некоторых людей, возможно, возникнет вопрос, почему GC недоступен в других средах, например, С или С++. Ответом являются типы. В этих языках программирования разрешено преобразование указателей на любой тип, что очень осложняет определение того, на что ссылается указатель. В управляемой среде, такой как CLR, гарантируется постоянство указателей, что делает возможным использование GC. Управляемый мир является единственным местом, где мы можем безопасно остановить выполнение потока, чтобы осуществить сборку мусора, в С++ эта операция и небезопасна, и очень ограничена.
Настройка для повышения скорости
Наибольшим беспокойством для программы в управляемом мире является сохранение данных в памяти. Некоторые проблемы неуправляемой среды не являются таковыми в управляемом мире: утечки памяти и указатели, указывающие на несуществующие объекты, здесь не проблема. Однако необходимо обратить особое внимание на ресурсы, которые остаются в памяти тогда, когда они уже не нужны.
Наиболее важным правилом для поддержания производительности и также самое легкое правило для программистов, которые пишут неуправляемый код: отслеживайте выделения памяти, которые необходимо сделать и освобождайте их, когда они больше не нужны. GC никак не может знать, что вы не собираетесь использовать созданную вами строку размером 20KB, если она является частью объекта, который остается в рабочем состоянии. Предположим, вы где-то сохраняете этот объект и никогда не собираетесь снова использовать эту строку. Обнуление поля даст возможность GC позже собрать эти 20KB, даже если этот объект до сих пор нужен вам для других целей. Если объект больше не нужен, убедитесь, что вы не сохраняете ссылки на него. Для более маленьких объектов это является меньшей проблемой.
Следующий важный аспект, влияющий на производительность, касается деталей очистки объекта. Как упоминалось ранее, финализация имеет огромное влияние на производительность. Наиболее общим примером является применение управляемого обработчика к неуправляемому ресурсу: вам надо применить некоторые методы очистки, и здесь производительность становится проблемой. Если вы зависите от финализации, вы открываетесь проблемам производительности, о которых говорилось ранее. Надо помнить еще о том, что GC практически совершенно не осведомлен о сжатии памяти в мире неуправляемого кода, таким образом вы можете использовать огромное количество ресурсов только сохраняя указатель в управляемой куче. Отдельный указатель не занимает много памяти, т.е. он может существовать некоторое время до того, как понадобится сборка мусора. Чтобы обойти эти проблемы производительности, вы должны выбрать схему разработки для всех объектов, требующих специальной очистки.
У программиста есть три варианта при работе с очисткой объекта:
- Реализуйте оба варианта
Рекомендуемая схема очистки объекта. Это объект с некоторой смесью неуправляемых и управляемых ресурсов. Примером будет System.Windows.Forms.Control. У него есть неуправляемый ресурс (HWND) и потенциально управляемый ресурс (DataConnection, и т.д.). Если вы не уверены в том, когда используются неуправляемые ресурсы, можно открыть манифест вашей программы в ILDASM и проверить наличие ссылок на неуправляемые библиотеки. Другой вариант – это воспользоваться vadump.exe, чтобы посмотреть, какие ресурсы загружаются вместе с вашей программой. С помощью обоих этих способов вы сможете понять, какие ресурсы используете.
Шаблон, приведенный ниже, предлагает пользователю отдельный рекомендуемый способ, вместо переопределения логики очистки (переопределения Dispose(bool)). Это обеспечивает максимальную гибкость. Сочетание максимальной скорости и гибкости наряду с безопасным подходом делает эту схему наиболее желательной.
Пример:
public class MyClass : IDisposable { public void Dispose() { Dispose(true); GC.SuppressFinalizer(this); } protected virtual void Dispose(bool disposing) { if (disposing) { … } … } ~MyClass() { Dispose(false); } }
- Реализуйте только Dispose()
Делайте это в случае, когда объект имеет только управляемые ресурсы, и вы хотите гарантировать, что его очистка детерминирована. Примером такого объекта является System.Web.UI.Control.
Пример:
public class MyClass : IDisposable { public virtual void Dispose() { … }
- Реализуйте только Finalize()
Требуется только в исключительно редких ситуациях, и я настойчиво рекомендую не пользоваться этим методом. Включение Finalize() свидетельствует о том, что программист понятия не имеет, когда объект должен быть убран сборщиком мусора, применяя при этом достаточно сложные ресурсы, требующие специальной очистки. Такая ситуация никогда не возникнет в хорошо разработанном проекте, если вы оказываетесь в подобной ситуации, необходимо вернуться и разобраться, что пошло не так.
Пример:
public class MyClass { … ~MyClass() { … }
- Ничего не реализуйте
Этот вариант применим к управляемым объектам, которые указывают только на другие управляемые объекты, которые и не являются удаляемыми, и не должны финализироваться.
Рекомендации
Рекомендации для работы с управлением памятью хорошо знакомы: освобождайте объекты после окончания работы с ними и не допускайте, чтобы оставались указатели на объекты. Когда доходите до очистки объекта с неуправляемыми ресурсами, реализовывайте оба метода: и Finalize(), и Dispose(). Это предотвратит непредсказуемость поведения в будущем и обеспечит хорошие навыки программирования.
Метод Dispose() должен поддерживаться объектами, которые используют неуправляемые ресурсы; однако метод Finalize() должен быть помещен только в те объекты, которые прямо используют эти ресурсы, например, OS Handle или распределение неуправляемой памяти. Рекомендуется создавать маленькие управляемые объекты, такие как «упаковщики», для реализации Finalize() в дополнение к поддержанию метода Dispose(), которые будут вызываться методом Dispose() родительского объекта. Т.к. родительские объекты не имеют финализатора, все дерево объектов не переживет сборку мусора невзирая на то, был вызван метод Dispose() или нет.
Хорошим правилом при работе с финализаторами является использование их только в наиболее примитивных объектах, которым необходима финализация. Помните: используйте метод Finalize() только там и тогда, когда вы должны.
JIT
Основные сведения
Как и любая VM, CLR нуждается в методе компилирования промежуточного языка в машинный код. Когда вы компилируете программу, чтобы запустить в CLR, компилятор преобразовывает ваш источник из языка высокого уровня в комбинацию метаданных MSIL (Microsoft Intermediate Language – промежуточный язык Microsoft). Все это собирается в РЕ файл, который затем может выполняться на любой имеющей CLR машине. При запуске этого исполняемого файла JIT начинает компилирование IL в машинный код и выполнение этого кода на реальной машине. Это делается на основе пометодной компиляции, т.е. при JIT компилировании длительность задержки зависит от размера кода, который вы хотите запустить.
JIT компилирование проходит очень быстро и генерирует очень хороший код. Некоторые осуществляемые им оптимизации (и некоторые пояснения каждой из них) обсуждаются ниже. Помните, что большинство из этих оптимизаций имеет ограничения, наложенные для обеспечения того, чтобы JIT не занимал слишком много времени.
- Свертывание констант — подсчитывает значения констант во время компиляции.
|
До |
После |
|
x = 5 + 7 |
x = 12 |
- Передача констант и копий — проводит обратное замещение, чтобы раньше освободить переменные.
|
До |
После |
|
x = a |
x = a |
|
y = x |
y = a |
|
z = 3 + y |
z = 3 + a |
- Замещение вызовов методов — Замещает args значениями, передаваемыми во время вызова, и устраняет вызов. Чтобы отсечь невыполняемый участок программы могут быть сделаны другие оптимизации. Из соображений скорости JIT имеет несколько ограничений, по которым он может замещать вызовы методов. Например, только маленькие методы могут быть замещены (размер IL менее 32), анализ управления потоками довольно примитивен.
|
До |
После |
|
…
x=foo(4, true);
…
}
foo(int a, bool b){
if(b){
return a + 5;
} else {
return 2a + bar();
} |
…
x = 9
…
}
foo(int a, bool b){
if(b){
return a + 5;
} else {
return 2a + bar();
} |
- Code Hoisting и Dominators— Удаляет код из внутренних циклов, если он дублируется во внешних. Пример цикла 'before', приведенный ниже, демонстрирует то, что действительно генерируется на уровне IL, т.к. все индексы массива должны быть проверены.
|
До |
После |
|
for(i=0; i< a.length;i++){
if(i < a.length()){
a[i] = null
} else {
raise IndexOutOfBounds;
}
} |
for(int i=0; i<a.length; i++){
a[i] = null;
} |
- Развертка цикла —Непроизводительные издержки возрастающих счетчиков и осуществления проверки могут быть удалены, и код цикла может быть повторен. Для крайне малых циклов это может привести к повышению производительности.
|
До |
После |
|
for(i=0; i< 3; i++){
print("flaming monkeys!");
} |
print("flaming monkeys!");
print("flaming monkeys!");
print("flaming monkeys!"); |
- Общее удаление подвыражения — Если активная переменная до сих пор содержит информацию, которая должна пересчитываться, замените ее.
|
До |
После |
|
x = 4 + y
z = 4 + y |
x = 4 + y
z = x |
Когда код должен быть JIT компилирован?
Здесь приведены этапы, которые проходит ваш код во время выполнения:
- Программа загружается, и инициализируется таблица функций с указателями, ссылающимися на IL.
- Главный (Main) метод путем JIT преобразуется в машинный код, который затем запускается. Вызовы функций компилируются в непрямые вызовы функций через таблицу.
- Когда вызывается другой метод, среда выполнения просматривает таблицу, чтобы проверить, нет ли указателей на код, обработанный JIT:
- Если указатель есть (возможно, он вызывается из другого вызова или был прекомпилирован), выполнение управляющей логики продолжается.
- Если нет, метод подвергается JIT компиляции и таблица обновляется.
- По мере вызова все больше и больше методов компилируются в машинный код, и больше элементов таблицы указывают на растущий пул процессорных инструкций.
- По мере выполнения программы JIT вызывается все реже и реже до тех пор, пока все не будет перекомпилировано.
- Метод не подвергается JIT до тех пор, пока он не вызван, и потом в течение выполнения программы он никогда не подвергается JIT повторно. Вы платите только за то, что используете.
Миф: программы, использующие JIT выполняются медленнее прекомпилированных программ
Это редкий случай. Затраты, связанные с обработкой JIT нескольких методов, незначительны в сравнении со временем, которое затрачивается на считывание нескольких страниц с диска, и методы подвергаются JIT только тогда, когда они необходимы. Время, затраченное на JIT, настолько мало, что оно практически всегда незаметно, и если метод однажды был подвергнут JIT, вы никогда не будете снова тратить время на его обработку.
Более важным является то, что JIT может выполнять некоторые оптимизации, которые недоступны обычным компиляторам, такие как оптимизации, характерные для CPU, и настройка кэша.
Оптимизации, присущие только JIT
Поскольку JIT активизируется во время выполнения, он использует большое количество информации, которую не может использовать компилятор. Это позволяет осуществлять некоторые оптимизации, которые доступны только во время выполнения:
- Процессор-специфические оптимизации — Во время выполнения JIT знает о том, может ли он использовать инструкции SSE или 3DNow. Ваш выполняемый файл будет скомпилирован специально для P4, Athlon или любого другого семейства процессоров. Будучи однажды созданным, тот же код будет совершенствоваться вместе с JIT и машиной пользователя.
- Удаление уровней преобразования логических адресов в физические, т.к. расположение функции и объекта доступны во время выполнения.
- JIT может осуществлять оптимизации через сборки, обеспечивая множество преимуществ, получаемых вами при компилировании программы со статическими библиотеками, но сохраняя гибкость и небольшие последствия использования динамических библиотек.
- Активные inline функции, вызываются чаще, т.к. во время выполнения учитывается управляющая логика. Оптимизации могут обеспечить существенное повышение скорости, и остается еще большое поле для улучшений в следующих версиях.
Перекомпилирование кода (использование ngen.exe)
Для производителей приложения привлекательна возможность прекомпиляции кода во время инсталляции. Компания Microsoft предоставляет эту возможность в форме ngen.exe, который позволит единожды запустить нормальный JIT компилятор по всей вашей программе и сохранит результат. Поскольку оптимизация только времени выполнения не может быть осуществлена во время прекомпиляции, генерируемый код не всегда так же хорош, как при нормальной JIT компиляции. Но из-за того, что не надо на лету обрабатывать JIT методы, затраты на запуск намного меньшие, и некоторые программы будут запускаться заметно быстрее. В будущем ngen.exe сможет делать больше, чем просто запускать ту же JIT компиляцию времени выполнения: более активные оптимизации, чем время выполнения, раскрытие оптимизации порядка загрузки разработчикам (оптимизация способа упаковки кода в VM страницы) и более сложные, требующие затрат времени оптимизации, которые могут воспользоваться преимуществом времени во время прекомпиляции.
Сокращение времени запуска помогает в двух случаях и для всего остального не может соревноваться с оптимизацией только времени выполнения, которую может делать обычная JIT компиляция. Первая ситуация – когда огромное количество методов вызывается рано в вашей программе. Вам придется подвергнуть JIT компиляции сразу много методов, что будет выражено в неприемлемо долгом времени загрузки. JIT прекомпиляция может иметь смысл, если это мешает вам, но не должна стать правилом. Прекомпиляция также имеет смысл в случае совместного использования больших библиотек, т.к. вы намного чаще тратите время на их загрузку. Microsoft прекомпилирует свои библиотеки для CLR, потому что большинство приложений будет их использовать.
ngen.exe легко использовать, чтобы увидеть полезна ли прекомпиляция для вас, рекомендуем попробовать воспользоваться ею. Однако в большинстве случаев действительно лучше использовать нормальную JIT компиляцию и пользоваться преимуществами оптимизации времени выполнения. Они обеспечивают огромный выигрыш времени, что более чем компенсирует затраты на загрузку в большинстве случаев.
Повышение производительности
Для программиста существует только две вещи, которые действительно ничего не стоят. Первое, то что JIT компиляция очень интеллектуальна. Не пытайтесь передумать компьютер. Пишите код так, как вам удобно. Например, предположим у вас есть следующий код:
|
…
for(int i = 0; i < myArray.length; i++){
…
}
…
|
…
int l = myArray.length;
for(int i = 0; i < l; i++){
…
}
…
|
Некоторые программисты верят, что они могут увеличить скорость тем, что уберут длинные расчеты и сохранят их во временные переменные, как в примере справа.
Истина в том, что такие оптимизации были полезны примерно в течение 10 лет: современные компиляторы способны осуществлять эти оптимизации. Кстати, иногда такие вещи могут действительно навредить производительности. В примере, приведенном выше, компилятор, вероятно, будет проверять то, что длина myArray постоянна, и вставит постоянную в сравнение цикла for. Но код справа может заставить компилятор думать, что это значение должно быть сохранено в регистре, т.к. l активна на всем протяжении цикла. Вывод таков: пишите наиболее читабельный и осмысленный код. Нет смысла стараться передумать компьютер, а иногда это может даже навредить.
AppDomains
Основные сведения
Межпроцессное взаимодействие становится все более и более распространенным. Из соображений стабильности и безопасности OS содержат приложения в разных адресных пространствах. Простым примером является способ, которым в NT выполняется 16-битное приложение: при запуске в отдельном процессе одно приложение не может пересекаться с выполнением другого. Проблемой здесь являются затраты на контекстные переключения и открытие связи между процессами. Эти операции имеют огромное негативное влияние на производительность. В серверных приложениях, которые обычно выполняют несколько web приложений, это основной непроизводительный расход как в производительности, так и в масштабируемости.
CLR представляет концепцию AppDomain, который похож на процесс, в котором для приложения есть модульное пространство. Однако AppDomains не ограничен одним процессом. Благодаря безопасности типов, предоставляемой управляемым кодом, есть возможность запуска двух совершенно независимых AppDomains в одном процессе. Для ситуаций, в которых вы обычно тратили большое количество времени выполнения на межпроцессное взаимодействие, выигрыш в производительности огромен: IPC между сборками в пять раз быстрее, чем между процессами в NT. Значительно уменьшая эти затраты, вы получаете и выигрыш в скорости, и новую возможность во время разработки программы: теперь имеет смысл использовать разделенные процессы там, где раньше это могло быть слишком дорого. Возможность запуска множества программ в одном процессе с той же системой безопасности, как раньше, имеет громадные последствия для масштабируемости и безопасности.
В OS нет поддержки для AppDomains. AppDomains обрабатывается хостом CLR, таким как представлен в ASP.NET, выполняемый оболочкой, или Microsoft Internet Explorer. Вы также можете написать собственный хост. Каждый хост определяет домен, применяемый по умолчанию, который загружается при первой загрузке приложения и закрывается только после завершения процесса. Когда вы загружаете другие сборки в процесс, вы можете определить, что они будут загружаться в определенный AppDomain, и установить различные политики безопасности для каждой из них. Это детально описано в документации Microsoft .NET Framework SDK.
Повышение производительности
Чтобы эффективно использовать AppDomains, вы должны подумать о том, какое приложение вы пишите, и какую работу оно должно выполнять. Применение AppDomains наиболее эффективно, если ваше приложение соответствует некоторым из следующих характеристик:
- Оно часто порождает собственную копию.
- Оно работает с другими приложениями, чтобы обрабатывать информацию (запросы баз данных внутри web сервера, например).
- Оно проводит много времени в IPC с программами, работающими исключительно с вашим приложением.
- Оно открывает и закрывает другие программы.
Пример ситуации, в которой полезны AppDomains, можно найти в сложном приложении ASP.NET. Предположим, что вы хотите усилить изоляцию двух различных vRoots: в собственном пространстве вам надо поместить каждый vRoot в отдельный процесс. Это, а также переключение контекста между ними, требует довольно больших затрат. В управляемом мире каждый vRoot может быть отдельным AppDomain. Это сохраняет требуемую изоляцию и полностью исключает непроизводительные издержки.
AppDomains – это то, что вы должны использовать только, если ваше приложение достаточно сложное и требует тесной работы с другими процессами или другими собственными экземплярами. Т.к. меж-доменная коммуникация намного более быстрая, чем коммуникация между процессами, затраты на запуск и закрытие AppDomain в действительности могут быть намного большими. AppDomains могут навредить производительности при неправильном использовании, поэтому убедитесь, что вы используете их в нужной ситуации. Обратите внимание, что только управляемый код может загружаться в AppDomain, т.к. нельзя гарантировать безопасность неуправляемого кода.
Сборки, которые совместно используются множеством AppDomains, должны быть JIT скомпилированы для каждого домена, для того чтобы сохранить изолированность доменов. В результате создается много дубликатов кода и идет пустое растрачивание памяти. Рассмотрите случай приложения, которое отвечает на запросы с помощью определенного XML сервиса. Если определенные запросы должны быть изолированы друг от друга, вы должны направлять их к разным AppDomains. В данном случае проблема в том, что каждому AppDomain теперь нужны одни и те же XML библиотеки, и одна и та же сборка будет загружаться много раз.
Единственным выходом из сложившейся ситуации является объявление Домен-нейтральной сборки. Это означает, что не допускаются прямые ссылки и изоляция поддерживается через преобразование логических адресов в физические. Это сохраняет время, т.к. сборка JIT компилируется только однажды. Это также бережет память, т.к. ничего не дублируется. К сожалению, из-за необходимости преобразования логических адресов в физические возникают потери производительности. Объявление сборки домен-нейтральной приводит к выигрышу в производительности только тогда, когда дело касается памяти или когда слишком много времени тратится на JIT компилирование кода. Такие сценарии распространены в случае большой сборки, которая совместно используется несколькими доменами.
Безопасность
Основные сведения
Безопасность доступа к коду является мощной, исключительно полезной характеристикой. Она предлагает пользователям безопасное выполнение ненадежного кода, защищает от злонамеренного программного обеспечения и некоторых видов атак, позволяет контролируемый, основанный на идентичности доступ к ресурсам. В машинном коде обеспечить безопасность очень сложно, т.к. очень низка безопасность типов и память обрабатывает программист. В CLR среда выполнения достаточно знает о запуске кода, чтобы добавить надежную поддержку безопасности. Для большинства программистов это является новшеством.
Безопасность влияет как на скорость, так и на размер working set приложения. И, как и в большинстве областей программирования, то, как программист использует систему безопасности, может иметь огромное влияние на производительность. Система безопасности разрабатывается с учетом производительности. Однако есть несколько вещей, которые вы можете сделать в системе безопасности, чтобы еще хоть немного повысить производительность.
Повышение производительности
Проверка безопасности обычно требует проверки стека вызовов, чтобы убедиться в том, что код, вызывающий текущий метод, имеет соответствующие допуски. Среда выполнения имеет несколько оптимизаций, которые дают возможность не проходить по всему стеку, но кое-что может сделать и программист. Это привело нас к упоминанию обязательной и декларативной безопасности: декларативная система безопасности присваивает типам ее членов различные права, в то время как обязательная система безопасности создает объект безопасности и осуществляет операции над ним.
- Декларативная безопасность является самым быстрым способом для Assert, Deny и PermitOnly.
- При осуществлении взаимодействия в неуправляемом коде, вы можете отменить проверки безопасности во время выполнения, используя атрибут SuppressUnmanagedCodeSecurity. Это перемещает проверку на время компоновки, что намного быстрее. Убедитесь, что код не вызывает ослабление безопасности в другом коде, который может использовать перемещенную проверку в небезопасном коде.
- Проверки на идентичность требуют больших затрат, чем проверки кода. Вы можете использовать LinkDemand, чтобы делать эти проверки во время компоновки.
Есть два способа оптимизировать систему безопасности:
- Осуществляйте проверки во время компоновки, а не во время выполнения.
- Делайте проверки безопасности декларативными, а не обязательными.
В первую очередь вы должны сконцентрироваться на переносе на время компоновки как можно большего количества этих проверок. Помните, что это может оказывать влияние на безопасность вашего приложения, поэтому убедитесь, что вы не перемещаете проверки в компоновщик, который зависит от состояния времени выполнения. Как только максимально возможное количество проверок перемещено на время компоновки, вы должны оптимизировать проверки времени выполнения, используя декларативную и обязательную безопасность: выберите, какая из них больше подходит для конкретного, выбранного вами, типа проверки.
Remoting
Основные сведения
Remoting технология в .NET распространяет богатую систему типов и функциональных возможностей CLR по всей сети. Используя XML, SOAP и HTTP вы можете вызывать процедуры и передавать объекты удаленно так, как будто они размещены на одном и том же компьютере. Вы можете рассматривать это, как .NET версию DCOM или CORBA, в которой реализован расширенный набор их функциональных возможностей.
Это исключительно полезно в серверной среде, когда вы имеете несколько серверов, размещающих различные сервисы, которые общаются друг с другом, чтобы обеспечить непрерывную связь этих сервисов. Также улучшается и масштабируемость, т.к. процессы могут физически размещаться на множестве компьютеров без потери функциональности.
Повышение производительности
Т.к. remoting часто имеет проблемы в показателях времени ожидания сети, в CLR применяются те же правила, как обычно: попытайтесь минимизировать поток посылаемой вами информации обмена и освободить программу от необходимости ожидания возвращения уделенного вызова. Вот некоторые правила, которым необходимо следовать при использовании удаления, чтобы увеличить производительность:
- Вместо большого количества небольших вызовов делайте единичные и емкие — если вы можете сократить количество вызовов, вы должны это сделать. Например, вы устанавливаете некоторые свойства для удаленного объекта с помощью get() и set() методов. Повторное создание объекта удаленно с установкой этих свойств при создании сохранит ваше время. Т.к. это может быть сделано с помощью одного удаленного вызова, вы сохраните время, потраченное на сетевой трафик. Иногда имеет смысл переместить объект на локальную машину, здесь установить свойства и затем скопировать объект обратно. Решение необходимо принимать в зависимости от пропускной способности и задержек.
- Сбалансируйте загруженность CPU и сети — иногда имеет смысл работать через сеть, а иногда лучше все делать самостоятельно. Если у вас уходит много времени на сетевые вызовы, будет страдать производительность. Если вы слишком загружаете CPU, вы не сможете отвечать на другие запросы. Сбалансированность между использованием CPU и сети является жизненно важной для масштабируемости вашего приложения.
- Используйте асинхронные вызовы — когда вы делаете вызов через сеть, убедитесь что он асинхронный, за исключением случаев, когда вам действительно нужны синхронные вызовы. В противном случае ваше приложение будет в состоянии ожидания до тех пор, пока не получит ответ, что может быть неприемлемым в пользовательском интерфейсе. Хороший пример приведен в Framework SDK, поставляемом с .NET, Samples\technologies\remoting\advanced\asyncdelegate.
- Оптимально используйте объекты — вы можете определить, чтобы новый объект создавался для каждого запроса (SingleCall) или чтобы один и тот же объект использовался для всех запросов (Singleton). Использование одного объекта для всех запросов конечно же менее ресурсоемко, но много усилий потребуется для синхронизации и конфигурации объекта от запроса к запросу.
- Используйте сменные каналы и средства форматирования — мощной возможностью удаления является возможность включения любого канала или средства форматирования в ваше приложение. Например, если вам не надо проходить через брандмауэр, нет причины использовать HTTP канал. TCP канал обеспечит вам гораздо лучшую производительность. Убедитесь, что вы выбрали наиболее подходящий канал или средство форматирования.
Типы значений
Основные сведения
Гибкость, доступная объекту, обеспечивается за счет очень небольших затрат производительности. Объекты, размещаемые в куче, требуют больше времени для распределения, доступа и обновления, чем объекты размещаемые в стеке. Это происходит потому, что, например, структура в С++ намного более эффективна, чем объект. Конечно, объекты могут делать то, что не под силу структуре, и намного более универсальны.
Но иногда вам не нужна вся эта гибкость. Иногда вы хотите использовать что-то настолько же простое, как структура, и не хотите лишних затрат производительности. CLR предоставляет возможность определить то, что называется типом значения, и интерпретируется во время компилирования как структура. Типы значений управляются стеком и предоставляют вам скорость структуры. Как и ожидалось, они также имеют ограниченную гибкость (например, у них нет наследования). Но в экземплярах, в которых вам нужно использовать структуру, типы значений обеспечивают невероятное повышение скорости.
Повышение производительности
Типы значений полезны только тогда, когда вы используете их как структуры. Если надо интерпретировать тип значений как объект, среда выполнения обработает для вас упаковку и распаковку объекта. Однако это требует даже больших затрат, чем создание его сразу как объекта!
Далее приведен пример простой проверки, проведенной для сравнения времени, необходимого для создания большого числа объектов и типов значений:
using System; using System.Collections; namespace ConsoleApplication{ public struct foo{ public foo(double arg){ this.y = arg; } public double y; } public class bar{ public bar(double arg){ this.y = arg; } public double y; } class Class1{ static void Main(string[] args){ Console.WriteLine("starting struct loop...."); int t1 = Environment.TickCount; for (int i = 0; i < 25000000; i++) { foo test1 = new foo(3.14); foo test2 = new foo(3.15); if (test1.y == test2.y) break; // prevent code from being eliminated JIT } int t2 = Environment.TickCount; Console.WriteLine("struct loop: (" + (t2-t1) + "). starting object loop...."); t1 = Environment.TickCount; for (int i = 0; i < 25000000; i++) { bar test1 = new bar(3.14); bar test2 = new bar(3.15); if (test1.y == test2.y) break; // prevent code from being eliminated JIT } t2 = Environment.TickCount; Console.WriteLine("object loop: (" + (t2-t1) + ")"); }
Сами проверьте этот пример. Разница во времени порядка нескольких секунд. Теперь давайте изменим программу таким образом, чтобы среде выполнения пришлось упаковывать и распаковывать нашу структуру. Обратите внимание, что преимущества в скорости от использования типов значений полностью исчезнут! Вывод: типы значений надо использовать только в исключительно редких ситуациях, когда вы не используете их, как объекты. Ознакомьтесь с этими ситуациями, поскольку выигрыш производительности при правильном использовании типов значений очень велик.
using System; using System.Collections; namespace ConsoleApplication{ public struct foo{ public foo(double arg){ this.y = arg; } public double y; } public class bar{ public bar(double arg){ this.y = arg; } public double y; } class Class1{ static void Main(string[] args){ Hashtable boxed_table = new Hashtable(2); Hashtable object_table = new Hashtable(2); System.Console.WriteLine("starting struct loop..."); for(int i = 0; i < 10000000; i++){ boxed_table.Add(1, new foo(3.14)); boxed_table.Add(2, new foo(3.15)); boxed_table.Remove(1); } System.Console.WriteLine("struct loop complete. starting object loop..."); for(int i = 0; i < 10000000; i++){ object_table.Add(1, new bar(3.14)); object_table.Add(2, new bar(3.15)); object_table.Remove(1); } System.Console.WriteLine("All done"); } } }
Типы значений широко используются в Microsoft: все простые типы являются типами значений. Использовать типы значений везде, где вы хотите использовать структуру. Если вы не будете упаковывать/распаковывать их, они обеспечат существенный выигрыш в производительности.
Еще одна очень важная вещь, о которой надо упомянуть, - это то, что типы значений не нуждаются в маршаллинге при взаимодействии. Поскольку маршаллинг является одним из наибольших поглотителей производительности при взаимодействии с машинным кодом, использование типов значений в качестве аргументов, будет единственной возможностью улучшить производительность.
|