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. Размещая управляемые сборки в разделяемой памяти, вы уменьшите вред
для других приложений на телефоне, наносимый "библиотечным скрежетом".
Это также уменьшит расход памяти в слоте вашего процесса.

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