MemMaker для .NET Compact Framework

Источник: Русский блог Windows Mobile
Оригинал статьи находится в блоге Роба Тиффани.

Кто-нибудь ещё помнит старые добрые времена DOS, когда мы проводили время, пытаясь выжать более 640Кб для драйверов, программ, резидентов и даже Windows? Такие вещи как QEMM, HIMEM, EMM386.EXE пробуждают у меня теплые воспоминания.


 

image
Некоторые из нас даже работали в OS/2, которая выделяла 740Кб для наших DOS-сессий, обеспечивая им вытесняющую многозадачность. Да, я мог запускать несколько DOS-игр одновременно в разных окнах без каких либо проблем. Удивительно, у меня была целая пачка защищённых от сбоев слотов, каждый из которых имел свои 740Кб памяти.

image

Возвращаясь в сегодняшний день, вы увидите, что Windows CE 5.0 и Windows Mobile 6.x имеют некоторое сходство со своими прадедушками из 80-х и 90-х. 32-битная встраиваемая операционная система, которой мы доверяем управлять нашими Windows телефонами, состоит из пачки слотов, но в отличии от DOS с её 640Кб, ваше приложение получает 32Мб виртуальной памяти. Однако, совершенно как в DOS у вас нет доступа ко всему адресному пространству, потому что другие вещи, такие как системные библиотеки это пространство уже используют.

Недавно я встретил своего хорошего друга Глена Джонса (Glen Jones), который хотел поделиться со мной некоторыми интересными находками. Имейте в виду, дело не только в том, что я считаю Глена и его коллег одними из лучших Compact Framework разарботчиков в мире. Его комманда спроектировала и разработала одно из самых больших и сложных приложений, работающих на Windows Mobile, используя Compact Framework. Как и любые другие компании, которые разрабатывают огромные Windows Mobile приложения, проблемы с нехваткой памяти не обошли и их. Брайан Пайк (Brian Pike), Один из архитекторов "команды мечты" недавно обнаружил, что если держать в памяти пустой exe-файл, а формы, код, ресурсы и данные хранить в отдельных управляемых сборках, то это уменьшает количество расходуемой виртуальной памяти в занимаемом им слоте, одновременно с этим получая возможность использовать память вне слота в рамках 1Гб разделяемой памяти.

Чтобы помочь нагляднее разобраться с этим невероятным открытием, я продемонстрирую две иллюстрации, на которых изображена карта использования памяти управляемым приложением, работающим в двух различных вариантах. Приложение, запущенное на эмуляторах, которые вы видите ниже, отображает использование 32 слотов разделяемой виртуальной памяти.Красным отмечена свободная память, синим — использованная, зеленым — зарезервированная. Слот №1 полон системных библиотек и вы не можете не заметить, что у каждого последующего слота есть небольшая синяя область. Это пространство используется системой и управляемыми библиотеками, что означает невозможность получить свои честные 32 мегабайта.

Слева вы видите Compact Framework приложение StandardExe.exe, запущенное в слоте №14. Это простое управляемое приложение содержит в себе битмап размером 2.25Мб в качестве ресурса и форму, на которой этот битмап находится. Если приглядеться, то видно, что 2.25 синих мегабайт находится в нижней части слота №14. Это пространство, занятое нашим exe-файлом.

стандартный способ оптимизированный способ

Справа находится OptimizedExe.exe, запущенное в слоте №11. Это совершенно пустой exe-файл. Функция Main вызывает метод статического класса в управляемой DLL и всё. В результате мы получаем exe-файл размером 5 килобайт. В управляемой сборке под названием OptimizedDLL.dll у нас находится всё тот же 2.25 мегабайтный битмап и форма. Если приглядеться к изображению справа, можно с удивлением обнаружить, что в слоте №11 нет никаких синих участков! А если смотреть ещё пристальнее, то 2.25 мегабайтную сборку невозможно обнаружить где-либо ещё.

Это достаточно здорово и значительно увеличивает наши возможности. Потенциально становится возможным разработать множество игр и приложений, которые ранее не видывали на Windows телефонах.Внимание, вопрос. Как это возможно? Это магия?

Те из вас, кто читал блог Стивена Пратшнера (Steven Pratschner), знают, что Compact Framework размещает управляемые exe и dll в первом гигабайте разделяемой памяти за пределами слота, в котором выполняется ваше приложение, что, безусловно, замечательно. А вот что вы могли и не знать, так это то, что операционная система автоматически блокирует в нижней части слота ровно столько памяти, сколько занимает ваш exe-файл. Таким образом, несмотря на то, что CLR контролирует выполнение приложения и в любовном порыве располагает его в разделяемой памяти, Windows CE отбирает драгоценную порцию памяти, потому как полностью уверена, что именно там и работает наше приложение. А ведь наше приложение управляемое и его там нет! Те из вас, у кого есть огромные управляемые exe-файлы, вы теряете огромный кусок памяти в своем слоте, который можно было бы использовать с гораздо большей пользой! Так что первый урок заключается в том, чтобы сделать то, что сделал Брайан — создать пустой exe-файл в виде заглушки, которая запускает приложение из отдельной управляемой сборки.

Ваш пустой exe-файл может выглядеть следующим образом:

using System;

namespace OptimizedExe
{
  static class Program
  {
    /// 
    /// The main entry point for the application.
    /// 
    [MTAThread]
    static void Main()
    {
      OptimizedDLL.StartUp.Main();
    }
  }
}


* This source code was highlighted with Source Code Highlighter.

Библиотека с приложением:

using System;
using System.Windows.Forms;

namespace OptimizedDLL
{
  public class StartUp
  {
    public static void Main()
    {
      Application.Run(new Main());
    }
  }
}


* This source code was highlighted with Source Code Highlighter.

Итак, мы научились обыгрывать Windows CE, играя по её же правилам. Теперь пора поговорить о любопытной ситуации с нашей управляемой сборкой. Если вы читали блог Рида Робинсона (Reed Robinson) про уничтожение монстра в памяти, вы, вероятно, знаете, что библиотеки отъедают память в слотах сверху вних, занимая всё больше и больше, что звучит не очень справедливо. Мы это называем "dll crunch" (скрежет библиотек), потому что свободная память остаётся между библиотеками и exe-файлом. Но у меня есть хорошие новости. Управляемые сборки себя так не ведут! По сути они не только не занимают ни один из слотов, они и ваш основной слот приложения не трогают. Как же это возможно?

Управляемые сборки не являются полноценными dll! CLR обращается с ними, как с обычными файлами и загружает их в гигабайт разделяемой памяти. Для Compact Framework управляемые сборки это просто файлы, полные IL-команд, которые не попадают в слот процесса. Теперь вы понимаете, где же находится 2.25 битмап, который мы поместили в OptimizedDLL.dll. Он за границей 32 мегабайтного барьера вашего слота, и тем самым он не расходует драгоценную память!

Так что же, если я последую этому новому способу, будет ли у меня когда-либо расходоваться виртуальная память, или я получил бесплатный обед?

Обед точно не бесплатный, но со скидкой. JIT компилятор выполняется в вашем слоте и подгружает IL-код, необходимый для текущего стека вызовов. Ресурсы, которые не нужно компилировать или выполнять, никогда не будут туда загружены. Куча сборщика мусора (GC Heap) также находится в вашем слоте и именно там зависают выделенные вами объекты. В вашем слоте содержится 64Кб стек для нитей (Thread Stack), которые порождает ваш процесс. Там же находится и куча домена приложения (AppDomain Heap), в которой содержатся представления структур, описанных в ваших сборках.

image
 

Итак, резюмируя вышесказанное.

1. Можно минимизировать ошибочное выделение неиспользуемой операционной системой памяти, следуя вышеописанному паттерну с расположением всего приложения внутри управляемых сборок, которое будет вызываться из пустого приложения-заглушки. Мы потеряем какие-то 5Кб. Спасибо, Брайан!

2. Размещая управляемые сборки в разделяемой памяти, вы уменьшите вред для других приложений на телефоне, наносимый "библиотечным скрежетом". Это также уменьшит расход памяти в слоте вашего процесса.

Перевод: Андрей Коновалов