CLR с изнанки

Новые средства и повышенная производительность в Silverlight 4

Эндрю Парду

Одним из самых крупных изменений в Silverlight 4 был перевод исполняющего движка на новую версию CLR. В каждом выпуске .NET Framework использовалась одна и та же CLR — от .NET Framework 2.0 до 3.5 SP1. В .NET Framework 4 произошли некоторые очень значительные изменения, такие как «вынос за скобки» Client Profile, который очень легко скачать, и уменьшение времени запуска за счет оптимизации структуры неуправляемых двоичных файлов. Но мы всегда были ограничены в свободе действий очень высокой планкой совместимости.

В .NET Framework 4 мы получили возможность внести существенные изменения в саму CLR, которая, тем не менее, по-прежнему остается высоко совместимой с предыдущими версиями. Silverlight 4 использует новую CLR как основу для своей CoreCLR и тем самым вводит все усовершенствования настольной версии в веб-версию. Некоторые из наиболее заметных усовершенствований в исполняющей среде — изменение в исходном поведении сборщика мусора (garbage collector, GC) и тот факт, что мы больше не осуществляем JIT-компиляцию двоичных файлов Silverlight Framework при каждом запуске программы Silverlight. В базовые классы мы внесли массу улучшений, в том числе усовершенствовали изолированное хранилище и изменили System.IO, который обеспечивает прямой доступ к файловой системе из приложений Silverlight, выполняемых с повышенными разрешениями.

Давайте начнем с краткого обзора того, как работает CoreCLR GC.

GC, учитывающий поколения объектов

CoreCLR использует тот же GC, что и настольная CLR. Это GC, учитывающий поколения объектов (generational GC), т. е. он работает по эвристическому алгоритму, в котором самые недавние созданные объекты скорее всего станут мусором к моменту его следующего сбора. Эта эвристика очевидна в малых областях видимости: локальные переменные функции станут недоступными сразу же после возврата управления этой функцией. Но, в целом, она применима и к более пространным областям видимости: программы обычно хранят некое глобальное состояние в объектах, которые существуют в течение всего времени выполнения программы.

Создаваемые объекты обычно относятся к самому молодому поколению (мы называем их поколением 0) и в течение сборов мусора переводятся в более старшие поколения (если вообще переживают сбор мусора) до тех пор, пока они не достигают максимально возможного поколения (поколения 2 в текущей реализации CLR GC).

В CLR GC появилось еще одно поколение — Large Object Heap (LOH). Большие объекты (а таковыми на данный момент считаются объекты, чей размер превышает 85 000 байтов) создаются непосредственно в LOH. Сбор мусора в этой куче выполняется в то же время, что и поколения 2.

Без учета поколений GC пришлось бы анализировать всю кучу, чтобы выяснить, к какой памяти происходят обращения, а какая память является мусором, до сбора неиспользуемой памяти. А при учете поколений GC не требуется перебирать всю кучу при каждом сборе. Так как длительность сбора прямо связана с размером собираемых поколений, GC оптимизирован так, чтобы реже выполнять сбор поколения 2 (и LOH). Сбор мусора в малых кучах осуществляется почти мгновенно и начинает занимать все больше времени по мере роста куч — сбор поколения 0 может проходить буквально за десятки микросекунд.

В большинстве программ поколение 2 и LOH намного больше по размеру, чем поколения 0 и 1, поэтому анализ всей памяти в этих кучах требует больше времени. Помните, что GC всегда собирает поколение 0, собирая поколение 1, и собирает все кучи, собирая поколение 2. Вот почему сбор поколения 2 называется полным сбором мусора. Более подробно о скорости сбора куч различных поколений см. в рубрике «CLR с изнанки» в октябрьском номере за 2009 г. по ссылке msdn.microsoft.com/magazine/ee309515.

Параллельный GC

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

Но часть работы сборщика мусора не изменяет память. Еще в первой версии CLR мы поддерживаем режим GC, при котором выполняются параллельные сборы. Это сборы, в которых выполняется большая часть работы полного сбора мусора без приостановки потоков программы на все время сбора.

Ряд операций GC может делать, не меняя никакие состояния, видимые программе, например GC может искать всю память, реально используемую программой. Потоки программы могут продолжать свою работу, пока GC анализирует кучи. А перед самым сбором мусора GC просто нужно выяснить, что изменилось с момента последней инспекции памяти, — скажем, если программа создала новый объект, его нужно пометить как доступный (reachable). В конце этих операций GC сообщает исполняющему движку блокировать все потоки — так же, как и при блокирующем сборе мусора, — и после этого переходит к обработке всей доступной памяти.

Фоновый GC

Параллельный GC всегда отлично работал в большинстве ситуаций, но есть один случай, для которого мы ввели значительные усовершенствования. Вспомните, что память выделяется в самом младшем поколении или в LOH. Поколения 0 и 1 находятся в одном сегменте — мы называем его эфемерным, так как в нем хранятся короткоживущие объекты. Когда эфемерный сегмент полностью заполняется, программа больше не может создавать новые объекты, так как для них нет места в этом сегменте. GC нужно выполнить сбор мусора в эфемерном сегменте, чтобы освободить некоторое пространство и обеспечить дальнейшее выделение памяти под объекты.

Проблема с параллельным GC в том, что он не может делать ничего из этих вещей, пока осуществляется параллельный сбор мусора. GC-поток не в состоянии перемещать какие-либо блоки памяти, пока работают потоки программы (поэтому он не может перевести более старые объекты в поколение 2), а поскольку сбор мусора уже идет, он не может начать сбор эфемерного сегмента памяти. Но GC нужно освободить какую-то память в эфемерном сегменте до того, как программа сможет продолжить свою работу. Потоки программы надо приостановить не потому, что параллельный GC изменяет видимое программой состояние, а потому, что программе негде выделять память. Если параллельный GC обнаруживает, что эфемерный сегмент заполнен, после того, как была найдена вся доступная память, он приостанавливает все потоки и выполняет блокирующее уплотнение.

Эта проблема объясняет мотивацию разработки фонового GC. Этот вариант сбора мусора работает так же, как параллельный в том плане, что GC всегда выполняет большую часть своих операций полного сбора в собственном фоновом потоке. Основное отличие в том, что это позволяет собирать эфемерную память, пока при полном сборе мусора обрабатываются данные. То есть программы могут продолжать работать, когда эфемерный сегмент заполняется. GC просто выполняет сбор эфемерной памяти, а все остальное выполняется обычным образом.

Влияние фонового GC на латентность программ очень значительна. При выполнении фонового GC мы наблюдали куда меньше пауз в работе программ, а те, что остались, были короче по времени.

Фоновый GC является режимом по умолчанию для Silverlight 4 и действует только на платформах Windows, так как в OS X недостает некоторой функциональности, необходимой GC для выполнения в фоновом или параллельном режиме.

Улучшения в производительности NGen

Компиляторы для управляемых языков вроде C# и Visual Basic не генерируют напрямую код, способный выполняться на аппаратном обеспечении. Они создают код на промежуточном языке — MSIL, который компилируется в исполняемый код при выполнении программы с помощью JIT-компилятора.

Применение MSIL дает массу преимуществ — от безопасности до портируемости, — но у JIT-компилируемого кода есть два недостатка. Во-первых, перед компиляцией и выполнением функции Main вашей программы нужно скомпилировать большой объем кода .NET Framework. Это означает, что пользователь должен ждать окончания JIT-компиляции, прежде чем программа начнет работать. Во-вторых, любой используемый код .NET Framework должен компилироваться для каждой программы Silverlight, выполняемой на компьютере.

Обе проблемы помогает решать NGen. NGen компилирует код .NET Framework во время установки, чтобы к моменту запуска вашей программы этот код уже был в скомпилированном виде. Код, компилируемый NGen, зачастую может использоваться несколькими программами совместно, благодаря чему рабочий набор на компьютере пользователя уменьшается при выполнении двух и более программ Silverlight. Если вы хотите узнать больше о том, как NGen сокращает время запуска и уменьшает рабочий набор, читайте рубрику «CLR с изнанки» в майском номере за 2006 г. по ссылке msdn.microsoft.com/magazine/cc163610.

Код из .NET Framework образует большую часть программ Silverlight, поэтому отсутствие NGen в Silverlight 2 и 3 приводило к заметной разнице во времени запуска. У JIT-компилятора уходило слишком много времени на оптимизацию и компиляцию библиотечного кода в стартовом пути каждой программы.

Наше решение этой проблемы заключалось в том, чтобы снять с JIT-компилятора бремя оптимизации генерации кода в Silverlight 2 и 3. Код по-прежнему нужно было компилировать, но, поскольку JIT-компилятор создавал простой код, на компиляцию уходило не слишком много времени. По сравнению с традиционными настольными приложениями большинство RIA-программ (Rich Internet Applications) имеют малый размер и выполняются не очень долго. Что еще важнее, они обычно являются интерактивными программами, т. е. большую часть времени проводят в ожидании пользовательского ввода. В сценариях, на которые ориентировались Silverlight 2 и 3, быстрый запуск был куда важнее, чем генерация оптимизированного кода.

По мере развития веб-приложений Silverlight мы вносили соответствующие изменения. Например, в Silverlight 3 мы добавили поддержку установки и выполнения приложений Silverlight как настольных. Обычно эти приложения крупнее и делают больше, чем малые интерактивные программы в классическом веб-сценарии. В саму Silverlight было добавлено много функциональности, интенсивно использующей вычислительные ресурсы, такой как поддержка сенсорного ввода в Windows 7 и манипуляций над фотоснимками (пример вы могли увидеть на веб-сайте Bing Maps). Все эти случаи требуют оптимизации кода для эффективного выполнения.

Silverlight 4 обеспечивает быстрый запуск и оптимизацию кода. JIT-компилятор теперь использует те же оптимизации в Silverlight, что и настольные .NET-приложения. Мы смогли добиться выполнения оптимизаций за счет введения поддержки NGen для .NET-сборок Silverlight. При установке Silverlight мы автоматически компилируем весь управляемый код в исполняющей среде Silverlight и сохраняем его на жестком диске. Когда пользователь запускает вашу программу Silverlight, она начинает выполнение без ожидания компиляции какого-либо кода из .NET Framework. Также очень важно, что теперь мы оптимизируем код в вашей программе Silverlight, чтобы она выполнялась быстрее, и мы можем обеспечить совместное использование кода .NET Framework несколькими программами Silverlight, работающими на одном компьютере.

При установке Silverlight 4 создает неуправляемые образы сборок .NET Framework. Для некоторых приложений важен максимально быстрый запуск, а остальное не имеет особого значения. Возьмите для примера Notepad (Блокнот): важно, чтобы он запускался быстро, но, как только вы начинаете набирать текст, вам уже без разницы, насколько быстро он выполняется (при условии, что он работает все же быстрее, чем вы печатаете текст). Для программ этого класса время, занимаемое JIT-компиляцией стартового кода приложения, может вызвать падение производительности. В Silverlight 4 большинство приложений запускается на 400–700 мс быстрее, чем в предыдущих версиях, и при их выполнении наблюдается 60% прироста производительности.

Base Class Library (BCL) — основной управляемый API, который теперь поддерживается NGen в Silverlight 4. Посмотрим, что нового в BCL.

Новая функциональность BCL

Многие новшества в BCL в Silverlight 4 также являются новшествами в .NET Framework 4, и о них уже писали в таком контексте. Я дам краткий обзор того, что включено в Silverlight 4.

Контракты кода обеспечивают встроенный способ выражения предусловий, постусловий и объектных инвариантов в вашем коде Silverlight. Эти контракты можно использовать для более эффективного выражения допущений в коде, и они помогают на ранних этапах отлавливать ошибки. В применении контрактов кода много дополнительных преимуществ. Подробности читайте в рубрике «CLR с изнанки» за август 2009 г. по ссылке msdn.microsoft.com/magazine/ee236408, на сайте Code Contracts DevLabs (msdn.microsoft.com/devlabs/dd491992) и в блоге группы BCL (blogs.msdn.com/bclteam).

Tuple-объекты чаще всего используются для возврата нескольких значений из метода. Они часто применяются в функциональных языках вроде F# и динамических языках наподобие IronPython, но их так же несложно использовать из Visual Basic и C#. Об архитектуре tuple-объектов см. в рубрике «CLR с изнанки» за июль 2009 г. (msdn.microsoft.com/magazine/dd942829).

Lazy<T> позволяет легко выполнять отложенную инициализацию объектов. Отложенная инициализация — это методика, с помощью которой в приложениях можно откладывать загрузку или инициализацию данных до первого обращения к ним.

В Silverlight 4 SDK в System.Numerics.dll доступны новые числовые типы данных BigInteger и Complex. BigInteger представляет целое число произвольной точности, а Complex — комплексные числа с вещественными и мнимыми частями.

Enum, Guid и Version теперь поддерживают TryParse подобно многим другим типам данных в BCL, позволяя эффективнее создавать экземпляр из строки; при этом в случае ошибок не генерируются исключения.

Enum.HasFlag — новый удобный метод для упрощения проверки состояния флага в перечислимом типе Flags; его использование не требует знания побитовых операций.

String.IsNullOrWhiteSpace — полезный метод, который проверяет, не является ли строка нулевой, пустой или содержит только символ любого пробела.

Переопределенные версии String.Concat и Join теперь принимают параметр IEnumerable<T>. Они обеспечивают конкатенацию любого набора, реализующего IEnumerable<T>, без предварительного преобразования набора в массив.

Stream.CopyTo упрощает чтение из одного потока данных и запись контента в другой поток данных; все это делается одной строчкой кода.

Помимо этих новых средств, мы также внесли усовершенствования в изолированное хранилище и разрешили доверяемым приложениям Silverlight напрямую обращаться к некоторым частям файловой системы через System.IO.

Усовершенствования в изолированном хранилище

Изолированное хранилище — это виртуальная файловая система, в которой приложения Silverlight могут хранить данные на клиенте. Чтобы узнать больше об изолированном хранилище в Silverlight, см. рубрику «CLR с изнанки» за март 2009 г. (msdn.microsoft.com/magazine/dd458794).

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

Мы также слышали от разработчиков жалобы насчет того, что нет простого способа переименования или копирования файлов внутри изолированного хранилища. Чтобы переименовать файл, приходилось вручную считывать содержимое исходного файла, создавать новый файл и записывать в него это содержимое, а потом удалять исходный файл. Переименование каталога осуществлялось аналогичным образом, но требовало написания еще большего объема кода, особенно если в нужном вам каталоге присутствовали подкаталоги. Такой способ работал, но был неэффективен по сравнению с тем, при котором вы просто сообщаете ОС переименовать какой-то файл или каталог на диске.

В Silverlight 4 к классу IsolatedStorageFile были добавлены новые методы, позволяющие выполнять перечисленные операции одной строкой кода: CopyFile, MoveFile и MoveDirectory. Мы также включили новые методы, предоставляющие дополнительную информацию о файлах и каталогах в изолированном хранилище: GetCreationTime, GetLastAccessTime и GetLastWriteTime.

Еще один новый API, добавленный в Silverlight 4, — IsolatedStorageFile.IsEnabled. Ранее единственный способ определить, разрешено ли применение изолированного хранилища, заключался в простой попытке воспользоваться им с перехватом последующего исключения IsolatedStorageException, которое генерировалось, если изолированное хранилище было отключено. Новое статическое свойство IsEnabled позволяет гораздо легче определять, включено ли изолированное хранилище.

Многие браузеры вроде Internet Explorer, Firefox, Chrome и Safari теперь поддерживают режим конфиденциального просмотра, в котором журнал посещений, файлы cookie и прочие данные не сохраняются. Silverlight 4 учитывает настройки этого режима, предотвращая доступ приложений к изолированному хранилищу и попытки сохранения информации на локальном компьютере, когда браузер работает в конфиденциальном режиме (private mode). В этих условиях свойство IsEnabled будет возвращать false, и любые попытки использовать изолированное хранилище будут давать исключение IsolatedStorageException.

Доступ к файловой системе

Приложения Silverlight выполняются в изолированной программной среде — «песочнице» (sandbox) — с частичным доверием. Эта «песочница» ограничивает доступ к локальной машине и накладывает на приложение ряд ограничений, не позволяющих злонамеренному коду нанести какой-либо вред. Например, частично доверяемое приложение Silverlight не может напрямую обращаться к файловой системе. Если приложению нужно сохранить данные на клиенте, то единственный вариант для него — использовать изолированное хранилище. Более широкий доступ к файловой системе возможен только через OpenFileDialog или SaveFileDialog.

В Silverlight 3 была добавлена возможность установки и выполнения приложений вне браузера. Это открывает возможность для некоторых интересных автономных вариантов применения, но такие приложения по-прежнему выполняются в той же «песочнице», что и приложения, работающие в браузере. Silverlight 4 позволяет настраивать приложения, работающие вне браузера, на выполнение с повышенным доверием. Такие доверяемые приложения способны обходить некоторые ограничения «песочницы» после установки. Например, доверяемые приложения могу обращаться к пользовательским файлам, использовать сетевые средства без ограничений на кросс-доменный доступ, обходить пользовательские разрешения и требования, а также получать доступ к неуправляемой функциональности ОС.

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

Доверяемые приложения могут использовать API-средства в System.IO для прямого доступа к следующим пользовательским каталогам в файловой системе: MyDocuments, MyMusic, MyPictures и MyVideos. Файловые операции вне этих каталогов в настоящее время запрещены и будут приводить к генерации исключения SecurityException. Но внутри перечисленных каталогов все файловые операции разрешены, в том числе чтение и запись. Скажем, доверяемое приложение — альбом фотоснимков может напрямую обращаться ко всем файлам в каталоге MyPictures, а доверяемый видеоредактор — сохранять фильмы в каталоге MyVideos.

Очень важно не «зашивать» в код приложений пути к этим каталогам, так как такие пути зависят от нижележащей ОС. Например, Эти пути абсолютно разные в Windows и Mac OS X, но они могут различаться даже между версиями Windows. Для корректной работы на всех платформах вы должны получать пути к этим каталогам через System.Environment.GetFolderPath. В следующем коде с помощью Environment.GetFolderPath мы получаем путь к каталогу MyPictures, находим через метод System.Directory.EnumerateFiles все файлы в MyPictures (и подкаталоги) с расширением .png, а потом добавляем каждый файловый путь в ListBox:

if (Application.Current.HasElevatedPermissions) {
  string myPictures = Environment.GetFolderPath(
    Environment.SpecialFolder.MyPictures);
  IEnumerable<string> files = 
    Directory.EnumerateFiles(myPictures, "*.png", 
    SearchOption.AllDirectories);
  foreach (string file in files) {
    listBox1.Items.Add(file);
  }
}

А этот код показывает, как создать текстовый файл в пользовательском каталоге MyDocuments из доверяемого приложения:

if (Application.Current.HasElevatedPermissions) {
  string myDocuments = Environment.GetFolderPath(
    Environment.SpecialFolder.MyDocuments);
  string filename = "hello.txt";
  string file = Path.Combine(myDocuments, filename);

  try {
    File.WriteAllText(file, "Hello World!");
  }
  catch {
    MessageBox.Show("An error occurred.");
  }
}

System.IO.Path.Combine вызывается при объединении пути к MyDocuments с именем файла; он вставляет соответствующий символ-разделитель, допустимый на данный платформе (в Windows используется \, а в Mac — /). File.WriteAllText создает файл (или перезаписывает его, если такой файл уже есть) и пишет в него текст «Hello World!».

Более высокая производительность и более широкие возможности

Как видите, в Silverlight 4 внесены усовершенствования как в исполняющую среду, так и в базовые классы. Новое поведение GC, возможность компилировать с помощью NGen сборки Silverlight Framework и улучшения в изолированном хранилище ускорят запуск и выполнение ваших приложений в Silverlight 4. Усовершенствования в BCL позволяют приложениям делать больше при меньшем объеме кода, а новые возможности, например доступ доверяемых приложений к файловой системе, облегчают применение новых потрясающих приложений.

Эндрю Парду (Andrew Pardoe) – руководитель программы в группе разработчиков CLR корпорации Майкрософт. Работает над многими аспектами исполняющего движка для Silverlight и настольной версий исполняющих сред. С ним можно связаться по адресу andrew.pardoe@microsoft.com..

Джастин ван Паттен (Justin Van Patten) — менеджер программ в группе Microsoft CLR. Работает над библиотеками BCL. С ним можно связаться через блог группы BCL по адресу https://blogs.msdn.com/b/bclteam/.

Выражаю благодарность за рецензирование статьи Сурупе Бисвасу (Surupa Biswas), Вэнсу Моррисону (Vance Morrison) и Маони Стефенс (Maoni Stephens)