Как правильно писать тесты 46 Цикл разработки 46 Структура проекта с тестами 51 Утверждения (Asserts) 52 Утверждения в форме ограничений 54 Категории 56

Вид материалаТесты

Содержание


Объединение управляемых модулей в сборку
Загрузка CLR при выполнении программы
Исполнение кода сборки
JIT-компилятор CLR
Вторая стадия компиляции
Подобный материал:
1   ...   4   5   6   7   8   9   10   11   ...   47

Объединение управляемых модулей в сборку




На самом деле среда CLR работает не с модулями, а со сборками. Сборка (assembly) — это абстрактное понятие, освоение которого поначалу может вызвать затруднения. Во-первых, это логическая группировка (коллекция) одного или нескольких управляемых модулей или файлов ресурсов. Во-вторых, это самая маленькая единица с точки зрения повторного использования, безопасности и управления версиями. Сборка может состоять из одного или нескольких файлов — все зависит от выбранных средств и компиляторов. В мире CLR сборка представляет собой то, что в других условиях называют компонентом.

Таким образом, сборка - это общее понятие, которое объединяет группу файлов в единую сущность.

Следующий рисунок помогает понять, что такое сборки: некоторые управляемые модули и файлы ресурсов (или данных) создаются инструментальным средством. Оно создает единственный файл РЕ32(+), который представляет логическую группировку файлов. При этом файл РЕ32(+) содержит блок данных, называемый декларацией (manifest). Декларация — просто один из наборов таблиц в метаданных. Эти таблицы описывают файлы, которые формируют сборку, общедоступные экспортируемые типы, реализованные в файлах сборки, а также относящиеся к сборке файлы ресурсов или данных.




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


Сборка позволяет разделить логический и физический аспекты компонента, поддерживающего повторное использование, безопасность и управление версиями. Разбиение кода и ресурсов на разные файлы отдано полностью на откуп разработчика. Так, редко используемые типы и ресурсы можно вынести в отдельные файлы сборки. Отдельные файлы могут загружаться из Интернета по мере надобности. Если файлы никогда не потребуются, они не будут загружаться, что сохранит место на жестком диске и ускорит установку. Сборки позволяют разбить на части процесс развертывания файлов и в то же время рассматривать все файлы как единый набор.

Модули сборки также содержат сведения о других сборках, на которые они ссылаются (в том числе номера версий). Эти данные делают сборку самоописываемой (self-describing). Иначе говоря, CLR знает о сборке все, что нужно для ее выполнения.


Загрузка CLR при выполнении программы7




Каждая создаваемая сборка представляет собой либо исполняемое приложение, либо DLL, содержащую набор типов (компонентов) для использования в исполняемом приложении. За управление исполнением кода, содержащегося в этих сборках, отвечает, конечно же, CLR. Это значит, что на компьютере, выполняющем приложение, должен быть установлен .NET Framework.


Понять, установлен ли .NET Framework на компьютере, можно, поискав файл MSCorEE.dll в каталоге %SystemRoot%\system32. Если он есть, то .NET Framework установлен. Заметьте: на одном компьютере может быть установлено одновременно несколько версий .NET Framework.


Прежде чем узнать, как загружается CLR, поговорим поподробнее об особенностях 32- и 64-разрядных Windows. Если сборка содержит только управляемый код с контролем типов, она должна одинаково хорошо работать на обеих версиях и дополнительной модификации исходного кода не требуется. Созданный компилятором готовый ЕХЕ- или DLL- файл будет выполняться как на 32-разрядной Windows, так и на версиях хб4 и 64-разрядной Windows. Иначе говоря, один и тот же файл будет работать на любом компьютере с .NET Framework.


При выполнении исполняемого файла Windows анализирует заголовок ЕХЕ-файла на предмет необходимого для его работы адресного пространства — 32-или 64-разрядного. Файл с заголовком РЕ32 может выполняться в адресном пространстве любого из указанных двух типов, а файлу с заголовком РЕ32+ требуется 64-разрядное пространство. Windows также проверяет информацию о процессорной архитектуре на предмет совместимости с имеющейся конфигурацией.


После анализа заголовка ЕХЕ-файла для выяснения того, какой процесс запустить — 32- или 64-разрядный, Windows загружает в адресное пространство процесса соответствующую (х86, х64) версию библиотеки MSCorEE.dll.


На этом процедура запуска управляемого приложения считается завершенной.


Исполнение кода сборки




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

Обычно разработчики программируют на высокоуровневых языках, таких как С# или Visual Basic. Компиляторы этих языков создают IL-код.


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


Для выполнения какого-либо метода его IL-код должен быть преобразован в машинные команды. Этим занимается JIT-компилятор CLR.

Теоретически только указанный компилятор является компонентом .NET, зависящим от конкретной платформы. Однако на самом деле от платформы также зависят и значительная часть библиотеки классов, и ряд других компонентов.


Вот что происходит при первом обращении к методу (см. след. рис).




Непосредственно перед исполнением Main среда CLR находит все типы, на которые ссылается код Main. При этом CLR выделяет внутренние структуры данных, используемые для управления доступом к типам, на которые есть ссылки. На рис. метод Main ссылается на единственный тип — Console, и CLR выделяет единственную внутреннюю структуру. Эта внутренняя структура данных содержит по одной записи для каждого метода, определенного в типе Console. Каждая запись содержит адрес, по которому можно найти реализацию метода. При инициализации этой структуры CLR заносит в каждую запись адрес внутренней функции, содержащейся в самой CLR (JITCompiler).


Когда Main первый раз обращается к WriteLine, вызывается функция JITCompiler. Она отвечает за компиляцию IL-кода вызываемого метода в собственные команды процессора. Поскольку IL компилируется непосредственно перед исполнением (just in time), этот компонент CLR часто называют JITter или JIT-компилятор (JIT-compiler). Функции JITCompiler известен вызываемый метод и тип, в котором он определен. JITCompiler ищет в метаданных соответствующей сборки IL-код вызываемого метода. Затем JITCompiler проверяет и компилирует IL-код в собственные машинные команды, которые сохраняются в динамически выделенном блоке памяти. После этого JITCompiler возвращается к внутренней структуре данных типа и заменяет адрес вызываемого метода адресом блока памяти, содержащего готовые машинные команды. В завершение JITCompiler передает управление коду в этом блоке памяти. Этот код — реализация метода WriteLine (той его версии, что принимает параметр String). Из этого метода управление возвращается в Main, который продолжает выполнение в обычном порядке.


Затем Main обращается к WriteLine вторично. К этому моменту код WriteLine уже проверен и скомпилирован, так что обращение к блоку памяти производится, минуя вызов JITCompiler. Отработав, метод WriteLine возвращает управление Main. След. рис. показывает, как выглядит ситуация при повторном обращении к WriteLine.





Снижение производительности наблюдается только при первом вызове метода. Все последующие обращения выполняются «на полной скорости»: повторная верификация и компиляция не производятся.

JIT-компилятор хранит машинные команды в динамической памяти. Это значит, что скомпилированный код уничтожается по завершении работы приложения. Так что, если потом снова вызвать приложение или если одновременно запустить второй его экземпляр (в другом процессе ОС), JIT-компилятор заново будет компилировать IL-код в машинные команды.

Для большинства приложений снижение производительности, связанное с работой JIT-компилятора, незначительно. Большинство приложений раз за разом обращается к одним и тем же методам. На производительности это скажется только раз. К тому же скорее всего больше времени занимает выполнение самого метода, а не обращение к нему.


Полезно также знать, что JIT-компилятор CLR оптимизирует машинный код аналогично компилятору неуправляемого кода С++. И опять же: создание оптимизированного кода занимает больше времени, но производительность его будет гораздо выше, чем неоптимизированного.


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

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

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


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

JIT-компилятор может обнаружить, что определенное выражение на конкретной машине всегда равно false. Например, посмотрим на метод с таким кодом:

if (numberOfCPUs > 1) {

}

Здесь numberOfCPUs — число процессоров. Код указывает JIT-компилятору, что для машины с одним процессором не нужно генерировать никаких машинных команд. В этом случае машинный код оптимизирован для конкретной машины: он короче и выполняется быстрее.

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


Существует утилита NGen.exe из состава .NET Framework SDK, которая компилирует весь IL-код выбранной сборки в машинный и сохраняет его в файле. При загрузке сборки в период выполнения среда CLR автоматически проверяет наличие предварительно скомпилированной версии сборки и, если она есть, загружает скомпилированный код, так что компиляция в период выполнения не производится. Заметьте, что NGen.exe не способна ориентироваться на конкретную среду выполнения и генерирует код для среднестатистической машины; по этой причине созданный утилитой код не столь оптимизирован, как произведенный JIT-компилятором.