Группа Microsoft .NET Framework всегда понимала, что повышение производительности, как минимум, столь же ценно для разработчиков, как и добавление новых функций исполняющей среды и библиотечных API. В .NET Framework 4.5 вложены значительные усилия, направленные на увеличение производительности во всех прикладных сценариях. Кроме того, поскольку .NET 4.5 является обновлением .NET 4, даже в приложениях для .NET 4 будут заметны многие улучшения производительности существующих в .NET 4 средств.
Когда разработчикам нужно обеспечивать удовлетворительные в отношении быстродействия характеристики приложений, по-настоящему важны время запуска (msdn.microsoft.com/magazine/cc337892), объем используемой памяти (msdn.microsoft.com/magazine/dd882521), пропускная способность и скорость реакции («отзывчивость»). Мы поставили цели улучшить эти показатели для различных сценариев применения. В этой статье я дам высокоуровневый обзор некоторых ключевых усовершенствований для повышения производительности, внесенных в .NET Framework 4.5.
CLR
В этом выпуске мы сконцентрировались на максимальном использовании возможностей многоядерных процессоров для повышения производительности, уменьшении латентности сборщика мусора (garbage collector) и улучшении качества кода неуправляемых образов. Ниже перечислены некоторые из ключевых средств, увеличивающих производительность.
Поддержка многоядерных процессоров JIT-компилятором Мы постоянно отслеживаем прогресс в аппаратном обеспечении и работаем в тесном сотрудничестве с производителями чипов, чтобы добиться максимально эффективной работы нашего программного обеспечения. В частности, многоядерные процессоры появились в наших лабораториях раньше, чем они стали доступны, и мы внесли соответствующие изменения, чтобы задействовать новые достижения в аппаратном обеспечении; однако поначалу выигрыш от этих изменений получали лишь очень немногие клиенты.
К этому времени, когда почти в любом компьютере установлен процессор минимум с двумя ядрами, новые средства, требующие наличия более одного ядра, становятся полезными широкому кругу разработчиков. В начале работы над .NET 4.5 мы запланировали определить, имеет ли смысл использовать ядра многоядерных процессоров для распределения на них задачи JIT-компиляции (особенно в процессе запуска приложений), чтобы ускорить общее быстродействие. В ходе исследований мы обнаружили, что во многих управляемых приложениях есть минимальное пороговое количество JIT-компилируемых методов, чтобы такие усилия имели смысл.
Эта функция обеспечивает параллельную JIT-компиляцию методов в фоновом потоке, который на многоядерном процессоре будет выполняться на другом ядре. В идеальном случае второе ядро быстро обгонит первое, выполняющее приложение, поэтому большинство JIT-компилируемых методов будет скомпилировано к тому моменту, когда они потребуются. Чтобы знать, какие методы стоит компилировать, данная функция генерирует данные профиля, с помощью которых отслеживаются выполняемые методы, а затем эти данные используются при последующем запуске приложения. Это требование генерации данных профиля — основной способ, которым вы взаимодействуете с этой функцией.
С минимальным добавлением кода вы можете использовать эту функцию исполняющей среды для значительного ускорения времени запуска как клиентских приложений, так и веб-сайтов. В частности, от вас требуется вызвать два статических метода класса ProfileOptimization в пространстве имен System.Runtime. Подробнее на эту тему см. документацию MSDN. Заметьте, что эта функция включена по умолчанию для приложений ASP.NET 4.5 и Silverlight 5.
Оптимизированные неуправляемые образы Вот уже в нескольких выпусках мы даем возможность предварительно компилировать код в неуправляемые образы (native images) с помощью утилиты Native Image Generation (NGen). Такие образы значительно ускоряют запуск приложений по сравнению с JIT-компиляцией. В этом выпуске мы ввели дополнительную утилиту — Managed Profile Guided Optimization (MPGO), которая оптимизирует структуру неуправляемых образов для еще большей производительности. MPGO использует технологию оптимизации на основе профилирования, очень похожую по своей концепции на описанную ранее поддержку многоядерных процессоров JIT-компилятором. При профилировании приложения применяется репрезентативный сценарий или набор сценариев, на основе которого структуры неуправляемого образа могут быть переупорядочены так, чтобы необходимые при запуске методы и структуры данных размещались с плотным прилеганием друг к другу в пределах одной части неуправляемого образа. Это уменьшает время запуска и сокращает рабочий набор (объем занимаемой приложением памяти). Исходя из нашего опыта и тестов, мы, как правило, наблюдаем выигрыш от MPGO в более крупных управляемых приложениях (например, в больших приложениях с интерактивным GUI) и рекомендуем применять эту утилиту именно для таких приложений.
Утилита MPGO генерирует данные профиля для DLL на промежуточном языке (IL) и добавляет этот профиль как ресурс в IL DLL. После профилирования с помощью NGen осуществляется предварительная компиляция IL DLL и выполняется дополнительная оптимизация благодаря наличию данных профиля. Схема этого процесса показана на рис. 1.
Увеличить
Рис. 1. Схема процесса при использовании утилиты MPGO
Application | Приложение |
MPGO | MPGO |
Application with Profile Data | Приложение с данными профиля |
NGen Install | NGen |
Optimized Application NGen Images | Оптимизированные NGen образы приложения |
Распределитель Large Object Heap (LOH) Многие .NET-разработчики просили решить проблему фрагментации LOH или предоставить способ принудительного уплотнения LOH. О том, как работает LOH, можно прочитать в рубрике «CLR с изнанки» за июнь 2008 г. (msdn.microsoft.com/magazine/cc534993). Если в двух словах, то память под любой объект размером 85 000 байтов и более выделяется в LOH. В настоящее время LOH не уплотняется. Уплотнение LOH потребовало бы много времени, так как сборщику мусора пришлось бы перемещать большие объекты, а это создавало бы высокие издержки. Когда объекты в LOH подпадают под сбор мусора, они оставляют свободные пространства между объектами, которые пережили сбор мусора, и это ведет к фрагментации.
CLR создает список свободной памяти из «мертвых» объектов, позволяя повторно использовать их, чтобы удовлетворить запросы на создание больших объектов; смежные мертвые объекты объединяются в один свободный. В конечном счете программа может оказаться в ситуации, когда фрагменты свободной памяти между «живыми» большими объектами недостаточно велики для дальнейшего выделения памяти под большие объекты в LOH, и, поскольку сжатие не предусмотрено, мы быстро попадаем в проблему. Это ведет к тому, что приложение начинает отвечать со все большими задержками, а потом возникают исключения, связанные с нехваткой памяти.
В .NET 4.5 мы внесли некоторые изменения для более эффективного использования фрагментов памяти в LOH, особенно в плане того, как мы управляем списком свободной памяти. Изменения относятся к сбору мусора (garbage collection, GC) как на рабочей станции, так и сервере. Но обратите внимание на то, что это не отменяет лимит в 85 000 байтов на размер объекта в LOH.
Фоновый GC для сервера В .NET 4 мы включили фоновый GC для рабочих станций. С того времени мы все чаще наблюдаем использование куч с размерами в диапазоне от нескольких до десятков гигабайт. Даже такому оптимизированному параллельному сборщику, как у нас, могут потребоваться секунды на сбор столь больших куч, а значит, потоки приложения будут блокированы примерно на то же время. Введение фонового GC для сервера обеспечивает поддержку нашим серверным сборщиком параллельных процедур сбора. Это сводит к минимуму длительные блокирующие операции сбора мусора, почти не влияя на высокую пропускную способность приложения.
Если вы используете серверный GC, вам не нужно ничего делать для того, чтобы задействовать преимущества этой новой функциональности; фоновый GC для сервера выполняется автоматически. Высокоуровневые характеристики фонового GC одинаковы как для клиента, так и для сервера:
- в фоне может выполняться только полный GC (объектов поколения 2);
- при фоновом GC сжатие не осуществляется;
- активный (не в фоне) GC (объектов поколений 0 и 1) возможен в процессе фонового GC. Серверный GC выполняется выделенными серверными GC-потоками;
- полностью блокирующий GC также осуществляется выделенными серверными GC-потоками.
Асинхронное программирование
Новая модель асинхронного программирования была введена как элемент Visual Studio Async CTP и теперь является важной частью .NET 4.5. Новые языковые средства в .NET 4.5 позволяют быстрее писать асинхронный код. Поддержку этой модели обеспечивают новые ключевые слова async и await в двух языках (C# и Visual Basic). .NET 4.5 также поддерживает асинхронные приложения, использующие эти новые ключевые слова.
Портал Visual Studio Asynchronous Programming на сайте MSDN (msdn.microsoft.com/vstudio/async) — отличный источник примеров, документов и обсуждений этих новых языковых средств.
Библиотеки параллельных вычислений
В .NET 4.5 был внесен ряд усовершенствований в библиотеки параллельных вычислений (parallel computing libraries, PCL), которые расширяют и улучшают существующие API.
Более быстрые облегченные задачи Классы System.Threading.Tasks.Task и Task<TResult> были оптимизированы так, чтобы они использовали меньше памяти и выполнялись быстрее в основных сценариях применения. В частности, скорость создания объектов Task и планирования продолжений увеличилась вплоть до 60%.
Большее количество параллельно выполняемых PLINQ-запросов PLINQ переключается на последовательное выполнение, когда считает, что распараллеливание данного запроса принесет только вред (замедлит его выполнение). Эти решения принимаются на основе взвешенных догадок и не всегда идеальны, поэтому PLINQ в .NET 4.5 будет распознавать больше классов запросов, которые можно успешно распараллеливать.
Более быстрые параллельные наборы В System.Collections.Concurrent.ConcurrentDictionary<TKey, TValue> внесен ряд тонких изменений, чтобы в определенных сценариях он работал быстрее.
Подробнее об этих изменениях читайте в блоге группы Parallel Computing Platform по ссылке blogs.msdn.com/b/pfxteam.
ADO.NET
Поддержка сжатия строк с null-битами Null-данные особенно часто встречаются у заказчиков, использующих функция разреженных полей (sparse columns feature) в SQL Server 2008. Потенциально они могут получать наборы результатов, содержащие большое количество null-полей. Для этого сценария была введена поддержка сжатия строк с null-битами (маркер SQLNBCROW, или просто NBCROW). За счет сжатия множества полей с NULL-значениями в битовую маску объем строк набора результатов, передаваемых с сервера и содержащих много null-полей, сокращается. Это значительно помогает функции сжатия в протоколе TDS (tabular data stream), где в данных присутствует много null-полей.
Entity Framework
Автоматически компилируемые LINQ-запросы Сегодня, когда вы пишете запрос LINQ to Entities, инфраструктура Entity Framework проходит все дерево выражений, сгенерированное компилятором C#/Visual Basic и транслирует (или компилирует) его в SQL-код, как показано на рис. 2.
Рис. 2. Запрос LINQ to Entities, скомпилированный в SQL-код
Компиляция дерева выражений в SQL, однако, приводит к некоторым издержкам, особенно в случае более сложных запросов. В предыдущих версиях Entity Framework, если вы хотели избежать таких издержек при каждом выполнении LINQ-запроса, вы должны были использовать класс CompiledQuery.
Новая версия Entity Framework поддерживает новую функциональность Auto-Compiled LINQ Queries. Теперь каждый выполняемый вами запрос LINQ to Entities автоматически компилируется и помещается в кеш плана запросов (query plan cache) Entity Framework. При каждом повторном выполнении запроса Entity Framework будет находить его в своем кеше запросов и не станет заново инициировать весь процесс компиляции. Подробнее об этом см. по ссылке bit.ly/iCaM2b.
Windows Communication Foundation и Windows Workflow Foundation
Группы Windows Communication Foundation (WCF) и Windows Workflow Foundation (WF) также внесли целый ряд усовершенствований в эти подсистемы, некоторые из которых перечислены ниже.
- Улучшения в масштабируемости TCP Клиенты сообщали о проблеме с TCP, когда множество параллельных пользователей посылает запросы с постоянным установлением новых соединений; служба совместного использования TCP-портов масштабировалась не слишком хорошо. Эта проблема устранена в .NET 4.5.
- Встроенная поддержка GZip-сжатия для WCF HTTP/TCP За счет этого новшества мы ожидаем сжатия вплоть до пятикратного.
- Поддержка асинхронной потоковой передачи через HTTP для WCF Мы реализовали эту функцию в .NET 4.5 и добились той же пропускной способности, что и в случае синхронной потоковой передачи, но с гораздо большей масштабируемостью.
- Уменьшение фрагментации поколения 0 для WCF TCP.
- Оптимизированный для больших объектов BufferManager Для больших объектов было реализовано более эффективное создание пула буферов, чтобы избежать высоких издержек GC поколения 2.
- Усовершенствование WF-проверки за счет кеширования выражений Как ожидается, это позволит добиться вплоть до трехкратного ускорения загрузки WF и ее выполнения в основном сценарии применения.
- Реализация в WCF/WF полноценной поддержки Event Tracing for Windows (ETW) Хотя этот функционал не относится к производительности, он помогает клиентам в анализе проблем с производительностью.
Более полные описания вы найдете в блоге группы Workflow Team (blogs.msdn.com/b/workflowteam) и в статье из MSDN Library по ссылке bit.ly/n5VCtU.
ASP.NET
Повышение плотности сайтов (site density) (также определяемой как потребление памяти на каждый сайт) и время «холодного» запуска сайтов в случае виртуального хостинга (shared hosting) — вот две основные цели в области производительности, которые ставились группой ASP.NET при работе над .NET 4.5.
В сценариях с виртуальным хостингом множество сайтов размещается на одной машине. В таких средах трафик обычно низкий. Данные, предоставляемые некоторыми компаниями-хостерами, показывают, что большую часть времени количество запросов в секунду ниже 1 с отдельными всплесками до 2 или более. Это означает, что многие рабочие процессы (worker processes) скорее всего будут уничтожены, если они простаивают длительное время (20 минут по умолчанию в IIS 7 и более поздних версиях). Таким образом, стартовое время становится очень важным. В ASP.NET это то время, которое требуется веб-сайту на то, чтобы принять запрос и ответить на него, когда рабочий процесс уже завершен.
Мы реализовали несколько средств в этом выпуске, чтобы уменьшить стартовое время в сценариях с виртуальным хостингом.
- Изолирование сборок в папке Bin (Bin assemblies interning) (при совместном использовании общих сборок) Функция теневого копирования в ASP.NET позволяет обновлять сборки, используемые в домене приложения, без выгрузки этого AppDomain (требуется потому, что CLR блокирует используемые сборки). Для этого сборки приложения копируются в отдельное место (определяемое либо CLR по умолчанию, либо пользователем) и загрузкой сборок из этого места. Это дает возможность обновлять оригинальную сборку в то время, как ее теневая копия блокирована. ASP.NET включает эту функцию по умолчанию для сборок в папке Bin, чтобы эти DLL можно было обновлять даже в процессе работы сайта.
ASP.NET распознает папку Bin веб-сайта как особый каталог для скомпилированных сборок (DLL) собственных элементов управления ASP.NET, компонентов или другого кода, на которые нужно ссылаться из приложения ASP.NET и параллельно использовать различными страницами сайта. На скомпилированную сборку в папке Bin можно автоматически ссылаться из любой части веб-приложения. ASP.NET также распознает новейшую версию конкретной DLL в папке Bin для использования веб-сайтом. Предварительно упакованные приложения, предназначенные для использования сайтами ASP.NET, обычно устанавливаются в папку Bin, а не в кеш глобальных сборок (Global Assembly Cache, GAC).
GC позволяет уменьшать объем памяти, используемый сайтом.
Группы ASP.NET и CLR обнаружили следующее. Когда множество сайтов размещается на одном сервере и использует одно приложение, многие из этих теневых копий DLL зачастую оказываются совершенно одинаковыми. Так как эти файлы считываются с диска и загружаются в память, это ведет к избыточным операциям загрузки, увеличивающим время запуска и объем используемой памяти. Мы работали над использованием символьных ссылок, которым следовала бы CLR, а затем реализовали идентификацию общих файлов и изолировали их в специальном месте (на которое будут указывать символьные ссылки). ASP.NET автоматически конфигурирует теневое копирование для DLL и других компонентов в папке Bin. Компании виртуального хостинга могут теперь настраивать свои машины согласно руководству ASP.NET для максимальной производительности. - JIT-компиляция с использованием многоядерных процессоров Соответствующую информацию см. в разделе «CLR» ранее в этой статье. Группа ASP.NET использует такую компиляцию для сокращения времени запуска за счет распределения рабочей нагрузки JIT-компиляции между ядрами процессора. Этот вариант включен по умолчанию в ASP.NET, поэтому вы можете задействовать преимущества этой функции безо всяких дополнительных усилий. Его можно отключить через параметр в файле web.config:
<configuration>
<!-- ... -->
<system.web>
<compilation profileGuidedOptimizations="None" />
<!-- ... -->
- Технология предвыборки (prefetcher) Эта технология в Windows очень эффективно сокращает издержки, связанные с чтением диска, в процессе запуска приложений. Предвыборка теперь поддерживается и в Windows Server (но не по умолчанию). Чтобы включить предвыборку для веб-хостинга с высокой плотностью сайтов, выполните следующий набор команд в командной строке:
sc config sysmain start=auto
reg add "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management\PrefetchParameters" /v EnablePrefetcher /t REG_DWORD /d 2 /f
reg add "HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Prefetcher" /v MaxPrefetchFiles /t REG_DWORD /d 8192 /f
net start sysmain
После этого вы можете обновить файл web.config, чтобы использовать предвыборку и в ASP.NET:
<configuration>
<!-- ... -->
<system.web>
<compilation enablePrefetchOptimization
="true" />
<!-- ... -->
- Настройка GC для веб-хостинга с высокой плотностью сайтов GC позволяет уменьшать объем памяти, используемый сайтом, но его можно настроить для большей производительности. Вы можете сконфигурировать GC для меньшей нагрузки на процессор (сократить частоту операций сбора мусора) или меньшего расходования памяти (увеличить частоту операций сбора мусора, чтобы быстрее освобождать память). Чтобы разрешить настройку GC, выберите параметр HighDensityWebHosting в файле aspnet.config в папке Windows\Microsoft\v4.0.30319 для уменьшения объема используемой памяти (рабочего набора) на каждый сайт:
<configuration>
<!-- ... -->
<runtime>
<performanceScenario
value="HighDensityWebHosting" />
<!-- ... -->
Более подробную информацию об усовершенствованиях в области производительности ASP.NET можно найти в документе «Getting Started with the Next Version of ASP.NET» по ссылке bit.ly/A66I7R.
Нам нужны ваши отзывы
Представленный здесь список усовершенствований далеко не полный. Множество менее масштабных улучшений было опущено, чтобы показать главное. Помимо этого, группы .NET Framework также работали над улучшениями производительности, специфичными для управляемых приложений Windows 8 в стиле Metro. Когда вы скачаете и опробуете .NET Framework 4.5 и бета-версию Visual Studio 11 для Windows 8, пожалуйста, дайте нам знать, если у вас появятся какие-либо замечания или предложения, чтобы мы могли учесть их в предстоящих выпусках.
Словарь терминов
Виртуальный хостинг (shared hosting) Также называется виртуальным веб-хостингом. Плотный веб-хостинг сотен, если не тысяч, веб-сайтов, выполняемых на одном сервере. За счет разделения затрат на оборудование расходы на поддержку каждого сайта можно свести к минимуму. Этот метод значительно понижает барьер входа для владельцев веб-сайтов.
Холодный запуск (cold startup) Время, необходимое на запуск приложения, когда его уже нет в памяти. Такое можно наблюдать, например, при запуске приложения после перезагрузки системы. В случае крупных приложений время холодного запуска может достигать нескольких секунд, так как в памяти отсутствуют необходимые страницы (код, статические данные, данные реестра и т. д.) и для их загрузки требуется обращения к дисковой подсистеме, что, естественно, замедляет процесс запуска.
Теплый запуск (warm startup) Время, необходимое на запуск приложения, когда оно уже находится в памяти. Например, если приложение было запущено несколько секунд назад, то, вполне вероятно, большая часть его страниц загружена в память и ОС будет повторно использовать их, экономя на дорогостоящих дисковых операциях. Вот почему приложение гораздо быстрее запускается при его повторном запуске (или почему второе .NET-приложение запускается быстрее первого — части .NET уже загружены в память).
Генерация неуправляемого образа (Native Image Generation, NGen) Относится к процессу предварительной компиляции исполняемых файлов с IL-кодом (Intermediate Language) в машинный код до их выполнения. Это дает два основных преимущества в производительности. Во-первых, уменьшается время запуска приложения, поскольку нет нужды компилировать код в период выполнения. Во-вторых, память используется эффективнее за счет возможности совместного использования страниц кода несколькими процессами. Также существует утилита, NGen.exe, которая создает неуправляемые образы и устанавливает их в Native Image Cache (NIC) на локальном компьютере. Исполняющая среда загружает неуправляемые образы, если они доступны.
Оптимизация на основе данных профиля (profile guided optimization) Такая оптимизация гарантированно ускоряет запуск и работу всех приложений — как управляемых, так и неуправляемых. Windows предоставляет инструментарий и инфраструктуру для выполнения оптимизации на основе данных профилей применительно к неуправляемым сборкам, а CLR — для выполнения аналогичной оптимизации применительно к управляемым сборкам (она называется Managed Profile Guided Optimization, или MPGO). Эти технологии используются многими группами в Microsoft для повышения производительности их приложений.
Сборщик мусора (garbage collector) Исполняющая среда .NET поддерживает автоматическое управление памятью. Она отслеживает каждую операцию выделения памяти, выполненную управляемой программой, и периодически вызывает сборщик мусора, который находит более не используемую память и делает ее доступной для новых операций выделения памяти. Важная оптимизация, выполняемая сборщиком мусора, заключается в том, что он не просматривает всю кучу каждый раз, а разделяет ее на три поколения (поколения 0, 1 и 2). Подробнее о работе сборщика мусора см. рубрику «CLR с изнанки» за июнь 2009 г. по ссылке msdn.microsoft.com/magazine/dd882521.
Уплотнение (compacting) Когда куча достигает пороговой фрагментации, сборщик мусора перемещает активные объекты ближе друг к другу. Главная цель уплотнения кучи — сделать возможным выделение более крупных блоков памяти под большее количество объектов.