Оптимизация запуска приложений .NET

ОГЛАВЛЕНИЕ

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

Запуски приложений обычно делят на холодные и горячие. Холодный запуск, в контексте управляемого приложения, значит, что ни сборки системы Microsoft® .NET Framework, ни код приложения не загружены в память, и их нужно получить с диска. Горячий запуск – это либо последующий запуск приложения, либо запуск приложения в случае, когда большинство системного кода уже в памяти, потому что использовался ранее другим управляемым приложением.

Холодный запуск

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

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

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

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

На рис. 1 вы можете сами убедиться в том, что холодный запуск зависим от ввода-вывода, запустив свое приложение два раза подряд. Первый запуск будет наверняка существенно медленнее второго (при котором большинство выполняемого кода уже находится в памяти после первого запуска, что экономит время на обращение к диску). Конечно, чтобы первый запуск действительно был холодным, нужно сперва перезагрузить компьютер, убедиться, что в папке запука нет управляемых приложений и что при входе пользователя в систему не запускается какая-либо служба Windows®, использующая управляемый код.

 

Рис.1 Время считывания с диска и время ЦП при холодном запуске 

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

Два счетчика производительности, которые можно использовать, чтобы узнать влияние холодного запуска на ввод-вывод, это % времени использования процессора и % времени считывания с диска. Если ввод-вывод оказывает решающее влияние на запуск, чего и следует ожидать, между % времени использования процессора и времени считывания с диска будет большая разница. Собрать счетчики производительности можно с помощью PerfMon (подробнее об этом см. боковую панель "Материалы о скорости запуска").

На рис. 1 красной линией обозначен % времени считывания с диска, а зеленой – % использования процессора. В случае холодного запуска видно, что использование ЦП невелико по сравнению с временем считывания с диска.

При повторном запуске приложения имеет место сценарий горячего запуска, поэтому счетчик производительности должен показать другую картину. На рис. 2 процесс зависит от ЦП. Можно видеть, что % времени считывания с диска значительно ниже % времени использования процессора.

 

Рис. 2 На горячий запуск затрачивается меньше времени 

Горячий запуск связан с ЦП, так как код уже находится в памяти (и не нужен дополнительный ввод-вывод), но перед запуском приложения код нужно JIT-компилировать. На сегодняшний день машинный код, создаваемый в .NET Framework за счет JIT, не сохраняется от одного вывполнения приложения до другого.

Если горячий запуск незначительно быстрее холодного, нужно выяснить, что загружает процессор (так как при горячем запуске большинство кода уже загружено и зависимость от ввода-вывода маловероятна). Причина должна лежать либо в большом объеме JIT-компилируемого кода, либо в сложных вычислениях, которое выполняет приложение.

Чтобы выяснить, связана ли проблема с JIT, можно воспользоваться счетчиком производительности .NET CLR JIT\% времени при JIT. Если значение небольшое (например, в основном больше 30%-40%), это значит, что JIT не оказывает решающего воздействия и нужно воспользоваться профайлером, чтобы определить, какие функции приложения потребляют основное время ЦП. Помните, что счетчик обновляется только тогда, когда методы непосредственно JIT-компилируются. Это значит, что после JIT-компиляции последнего метода счетчик будет отображать последнее значение, а не обнулится. Поэтому на счетчик следует смотреть только первые несколько секунд запуска приложения. В это время счетчик будет увеличиваться очень быстро, означая, что пик использования ЦП вызван компилятором JIT.

Следует иметь в виду, что любое приложение, запускающееся при входе пользователя в систему, вынуждено конкурировать за ввод-вывод с другими службами и приложениями, что еще больше увеличивает время запуска. Поэтому старайтесь не добавлять приложения в группу запуска при загрузке (хорошее средство определить, какие приложения запускаются при загрузке компьютера -- AutoRuns, которое можно найти по адресу microsoft.com/technet/sysinternals/Security/Autoruns.mspx). 


Определение кода, загружаемого с диска

Следующий шаг -- определить, что загружается с диска и выяснить, нет ли кода, который загружается без надобности. Самый быстрый способ определить, что загружается в память -- средство VADump (его можно найти в пакете Windows Platform SDK). На рис. 3 показан фрагмент отчета, создаваемого при выполнении следующей команды:

Figure 3 VADump Output

Category            Total    Private Shareable  Shared
              Pages  KBytes  KBytes  KBytes  KBytes
    Page Table Pages    177    708    708     0     0
    Other System      39    156    156     0     0
    Code/StaticData    8169   32676   2160   8336   22180
    Heap         14042   56168   56168     0     0
    Stack          0     0     0     0     0
    Teb           0     0     0     0     0
    Mapped Data       8    32     0     4    28
    Other Data        1     4     4     0     0

    Total Modules     8169   32676   2160   8336   22180
    Total Dynamic Data  14051   56204   56172     4    28
    Total System      216    864    864     0     0
Grand Total Working Set  22436   89744   59196   8340   22208

Module Working Set Contributions in pages
   Total  Private Shareable  Shared Module
    72     2    70     0 HeadTrax - HeadTrax.exe
    107     7     0    100 ntdll.dll
    37     4     6    27 mscoree.dll
    77     3     0    74 KERNEL32.dll
     6     2     0     4 LPK.DLL
    27     4     0    23 USP10.dll
    116     4     0    112 comctl32.dll
    878    23    79    776 mscorwks.dll    

Heap Working Set Contributions
  0 pages from Process Heap (class 0x00000000)
  0 pages from Process Heap (class 0x00000000)
9332 pages from Process Heap (class 0x00000000)
  0x0255850F - 0xC255350F 9332 pages
  0 pages from Process Heap (class 0x00000000)
  0 pages from Process Heap (class 0x00000000)
4710 pages from Process Heap (class 0x00000000)
  0x00040000 - 0x10040000 4710 pages
  0 pages from Process Heap (class 0x00000000)

Stack Working Set Contributions
  0 pages from stack for thread 00001018
  0 pages from stack for thread 000017EC
  0 pages from stack for thread 0000187C

VADump –sop <proc ID> 

Важно помнить, что VADump показывает, что загружено в память, только при работе средства, поэтому оно может пропустить модули, загружаемые в память на короткое время. Оно также не показывает ту часть приложения (код или данные), которая была записана на диск. Задача состоит в том, чтобы на основании отчета VADump определить, нужно ли загружать все модули в списке. Например, если приложение не использует XML, а System.Xml оказался загружен, нужно разобраться.

Выяснить, что загрузило сборку, можно с помощью команды sxe в отладчике Windows (windbg). Команда "sxe ld:< имя dll>" прерывает работу отладчика при загрузке указанного DLL. Затем можно проверить стек вызовов и выяснить, какая функция загрузила DLL в память. Не следует недооценивать эту часть исследования. Очень легко потерять контроль за тем, что приложение загружает в память. 


Сборки системы и другие процессы

Исключив загрузку лишних сборок при запуске (дополнительно можно изменить код приложения так, чтобы отложить часть инициализационных процессов), следующим шагом нужно уменьшить объем кода, загружаемого из сборок системы. Я, к сожалению, не знаю средств, которые бы сообщали, сколько загружается кода при использовании системного API. Это было бы весьма полезно, так как разработчик мог бы использовать в коде запуска те API, которые загружают из сборок системы меньше кода. Пока таких средств нет, можно оценить приблизительную нагрузку API с помощью профайлера на основе инструментов (например, Visual Studio® Performance Tools).

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

В некоторых случаях приложение может явно или неявно запускать при загрузке другие процессы. Выяснить это нетрудно с помощью параметра –o отладчика Windows (windbg). За счет параметра –o отладчик присоединяется к любому дочернему процессу. Типичный пример процесса, неявно запускаемого приложением -- когда приложение использует сериализацию XML и не прекомпилирует классы сериализации (с помощью служебной программы Sgen). В этой ситуации для их компиляции запускается компилятор C#. Запуск дополнительных процессов – очень затратное дело, это может иметь большое влияние на скорость загрузки.


Производительность NGen

Генератор машинных образов (NGen) всегда помогает ускорить горячий запуск, потому что позволяет избежать JIT-компиляции кода. NGen может помочь и в случае холодного запуска, если не нужно загружать mscorjit.dll, потому что весь код, ипсользуемый приложением уже заранее скомпилирован с помощью NGen. Однако, если хотя бы один из модулей не имеет соответствующего машинного образа, mscorjit.dll будет загружен. Тогда будет JIT-компилироваться код, занимая циклы ЦП, а также будут затронуты многие страницы образов NGen, так как компилятор JIT должен считать метаданные. Время запуска окажется в итоге еще больше. Поэтому стоит удалить весь код, который может вызвать JIT-компиляцию при запуске. Конечно, следует ли использовать этот подход, можно решить, только измерив скорость холодного запуска с и без создания машинных образов, потому что итоговое преимущество NGen при холодном запуске зависит от кода и размера приложения; нет гарантий ощутимого улучшения запуска, даже если исключено JIT-компилирование.

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

COMPLUS_MDA=JitCompilationStart

Приложение остановит отладчик при JIT-компиляции кода. MDA можно также установить с помощью реестра или файла .config приложения. Узнать больше об использовании MDA можно в боковой панели «Материалы о скорости запуска» .

В общем случае, чтобы NGen действительно увеличил скорость холодного запуска, нужно обеспечить следующее:

  • Все приложение обработано NGen.
  • Нет модификаций базового адреса. Модификация базового адреса – очень ресурсоемкая операция, и код после этого не может быть общим. Дополнительную информацию о том, как установить базовый адрес, можно найти по адресу msdn.microsoft.com/msdnmag/issues/06/05/CLRInsideOut.
  • Сборки установлены в глобальный кэш сборок (GAC). Проверка строгих имен затрагивает весь файл, но пропускается для всех сборок, установленных в GAC.


Проверка Authenticode

Сборки можно подписать с помощью сертификата Authenticode, используя средство signcode. Проверка Authenticode всегда негативно влияет на запуск, так как подписанные с помощью Authenticode сборки должны проверяться центром сертификации (CA). Проверка требует перепроверки CA, использованного для подписи сборок, а это очень ресурсоемкая операция, связанная с доступом в сеть (если CA не установлен локально на том же компьютере).

Лучше всего избегать сборок, подписанных с помощью Authenticode и использовать вместо них подписи строгими именами. Если подпись Authenticode необходима, в .NET Framework 3.5 проверку можно опустить с помощью следующего параметра конфигурации:

<configuration>
   <runtime>
     <generatePublisherEvidence enabled="false"/>
   </runtime>
</configuration>

Заметьте, однако, что даже в том случае, когда необходима подпись Authenticode, время проверки подлинности можно сильно сократить, просто установив сертификат CA на компьютер клиента.

Выводы

Получить хорошую скорость холодного запуска можно, в первую очередь, за счет лаконичности кода, выполняемого при запуске. Нужно отложить инициализацию, которая не является строго необходимой, проверить, чтобы ссылки не загружались слишком рано и попытаться использовать такие классы и методы, которое не загружают много кода. Помните, задача – сократить обращения к диску. Это нелегко, но Xperf, новое, полезное средство, включенное в новый разрабатываемый Windows Server® 2008 SDK, использует трассировку событий для Windows (ETW) для слежения за загруженными модулями, переключениями контекста и другими событиями, чтобы лучше определять, что происходит при запуске приложения. С помощью XPerf станет возможно собирать очень точные данные о времени запуска приложений. Боковая панель «Материалы о скорости запуска» содержит ссылки на дополнительную полезную информацию.

Клаудио Калдато (Claudio Caldato) руководитель проекта разработки Сборщика мусора в команде CLR корпорации Майкрософт.