0x00 Описание

Многие люди часто задаются вопросом, почему данные памяти, предоставляемые профилировщиком Unity, отличаются от данных памяти, предоставляемых некоторыми собственными профилировщиками, такими как Xcode для iOS. И людей интересует, как анализировать данные из этих собственных инструментов, таких как Xcode для iOS. Смотрел сеанс Разработка и оптимизация процедурной игры | The Elder Scrolls Blades - Объедините Копенгаген недавно. На сессии были затронуты некоторые темы, связанные с памятью iOS и Unity. И в прошлом году была потрясающая сессия WWDC2018, вы можете найти несколько полезных ссылок в конце статьи. Однако, что касается разработчиков Unity, что нам делать, чтобы решить проблемы, связанные с памятью iOS?

Далее я расскажу, как разработчик Unity может решать проблемы, связанные с памятью iOS, на работе. Основное содержимое включает в себя анализ управления памятью системы iOS, использование инструмента для просмотра состояния памяти в игре Unity и использование инструментов командной строки для более глубокого изучения проблем с памятью в игре Unity.

0x01 Управление памятью iOS - неправильно ли Unity Profiler?

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

В зависимости от того, какой инструмент мы использовали, конечный результат может отличаться. Следовательно, если вы ищете число, чтобы суммировать всю информацию о памяти приложения или игры, вы можете упростить задачу или проигнорировать сложность операционной системы. Например, разные версии iOS имеют разную статистику по накладным расходам памяти. Количество используемой памяти металлического приложения, работающего на iOS12, больше, чем у iOS11, в датчике памяти Xcode. Это связано с тем, что Apple изменила статистическую стратегию для памяти, и большая часть памяти, которая ранее не учитывалась, теперь также учитывается в накладных расходах памяти.

Учет очищаемой энергонезависимой памяти изменился, начиная с iOS 12 и tvOS 12. В iOS 11 и tvOS 11 выделение памяти в этом режиме хранения памяти, обычно используемом приложениями Metal для хранения буферов, текстур и объектов состояния, не выполнялось. t учитывались в пределе памяти приложения и не отображались в таких инструментах, как Xcode memory gauge.



Также на платформе iOS данные из датчика памяти Xcode и данные из Instrument могут не совпадать. А ранний инструмент распределения инструментов в основном использовался для подсчета памяти кучи. Поэтому вместо того, чтобы тратить время на сравнение данных из разных инструментов, лучше использовать один и тот же инструмент для измерения накладных расходов памяти или определения эффективности оптимизации памяти.

Итак, важно понимать, как операционная система управляет памятью и как интерпретировать данные, предоставляемые инструментом профилирования. Затем давайте обсудим механизм управления памятью в системе iOS, а затем посмотрим на данные памяти, захваченные Xcode, и данные памяти, захваченные Unity.

Во-первых, у каждого процесса будет адресное пространство. Его диапазон поддерживается размером указателя, например 32-битным или 64-битным. И адресное пространство сначала делится на несколько областей, а затем подразделяется на страницы размером 4 КБ (ранняя версия) или 16 КБ (после A7) по размеру страницы, эти страницы наследуют различные атрибуты региона, такие как доступ только для чтения, доступный для чтения и с возможностью записи и т. д. Конечно, на некоторых страницах может храниться меньше данных, чем размер страницы, а для хранения некоторых данных может потребоваться несколько страниц. Таким образом, накладные расходы на память вашего приложения или игры равны количеству страниц, умноженному на размер страницы.

Конечно, в системе есть и реальная физическая память.

Виртуальная память против резидентной памяти

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

Как показано на рисунке выше, приложение выделяет память, в виртуальной памяти выделяются 4 области, а третья область включает 13 страниц. Но на данный момент только 6 страниц фактически сопоставлены с физической памятью. Сопоставление виртуальной памяти с реальной физической памятью происходит при первом использовании памяти, например при чтении данных из памяти или записи данных в память. Резидентная память также является виртуальной памятью, но эта часть виртуальной памяти сопоставлена ​​с реальной физической памятью. Я думаю, вы можете увидеть похожие данные в Xcode или Instrument. Например, в инструменте отслеживания виртуальных машин Instrument отображается резидентный и виртуальный размер соответственно.

Грязная память против чистой памяти

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

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



В дополнение к этому, есть разделы __TEXT исполняемых файлов и некоторые разделы DATA CONST фреймворков, которые также классифицируются как чистая память.

На WWDC2018 инженеры Apple привели очень яркий пример. То есть выделяется массив из 20 000 целых чисел и создаются страницы. Если назначены только первый элемент и последний элемент, первая и последняя страница, то есть страницы, на которых расположены первый и последний элементы, станут грязными, но страницы между первым и последним будут по-прежнему чистыми.

Сжатая память

Когда требуется больше памяти, система отбрасывает чистую страницу. Но грязную страницу нельзя выбросить, а что, если грязной памяти слишком много? До iOS 7, если у процесса было слишком много грязной памяти, система немедленно завершала процесс. После iOS 7 был представлен механизм сжатой памяти. Поскольку в iOS нет традиционного механизма подкачки диска (в Mac OS есть), размер подкачки, который мы видели в профилировщике Apple, на самом деле является сжатой памятью.

Начиная с iOS 7, операционная система может сжимать грязную память с помощью компрессора памяти. Компрессор памяти сжимает грязные страницы (несколько страниц), к которым не обращались какое-то время. Однако при повторном доступе к этой памяти компрессор памяти распакует ее для надлежащего доступа.

Unity Profiler ошибается?

Можно видеть, что с точки зрения управления памятью операционной системы память процесса на самом деле очень сложна. Данные памяти, записанные Unity, например, «Зарезервированное общее количество - Unity», в основном взяты из записи MemoryManager в движке. MemoryManager вызовет соответствующий распределитель, чтобы выделить память для движка в соответствии с различными ситуациями.

Например, мы можем использовать бесплатный проект Unity 3D Game Kit в качестве примера, используя Instrument, чтобы проверить его выделение памяти.

Вы можете видеть, что MemoryManager вызывает UnityDefaultAllocator. На рисунке ниже показано, что IphoneNewLabelAllocator вызывается для выделения памяти.

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

0x02 Используйте инструмент для проверки памяти, используемой в игре Unity

В этой части я рекомендую статью Валентина Симонова Понимание памяти iOS (WiP) », в которой рассказывается об использовании некоторых инструментов для проверки памяти, используемой в игре на Unity.

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

Помимо использования Instrument для исследования проблем с памятью, мы также можем профилировать проблемы с памятью с помощью замечательного инструмента отладчика памяти Xcode. В частности, после экспорта файла Memgraph вы можете использовать различные инструменты командной строки, чтобы помочь в расследовании и получить дополнительную информацию.

И иногда люди будут жаловаться, что данные памяти, отображаемые на странице отчета о памяти Xcode, не только отличаются от Unity Profiler, но иногда даже отличаются от собственных инструментов производительности Apple, таких как Instrument. Как упоминалось выше, разные инструменты могут иметь разные данные. Но мы также можем использовать файл Memgraph и инструмент командной строки, чтобы проверить, на что ориентированы данные отчета о памяти.

По-прежнему используя проект Unity 3D Game Kit в качестве демонстрации, тестовым устройством является iPhone X, но перед тем, как мы начнем, нам нужно включить опцию Scheme - ›Run -› Diagnostics - ›Malloc Stack.

После запуска игры нажмите «Начать игру», чтобы загрузить первую сцену, и в отчете о памяти мы видим, что на данный момент объем памяти достиг 1,48 ГБ. Однако индикатор памяти по-прежнему находится в зеленой части, поэтому тот факт, что индикатор памяти не является хорошим предложением по оптимизации, потому что эти накладные расходы памяти на iPhone7 напрямую приведут к завершению игры системой.

Утечка анимации?

Переходим непосредственно к отладчику памяти Xcode. Если вы хотите проверить, есть ли здесь утечка памяти, вы можете выбрать опцию в фильтре. Здесь часто встречается ситуация с «ложной утечкой».

Если мы посмотрим на его стек вызовов, он в основном связан с анимацией. Я проконсультировался с разработчиком из команды Animation-Dev по этому вопросу, подтвердив, что Xcode сообщает о ложной утечке памяти в этом случае, и блок памяти по-прежнему ссылается на распределитель. Эта память будет освобождена, когда будет освобожден весь блок.

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



Затем вы можете экспортировать данные в виде файла .memgraph и использовать некоторые инструменты командной строки для их обработки.

Сводка VMMAP

Первый инструмент командной строки - это vmmap, который позволяет нам просматривать текущие данные виртуальной памяти.

Когда мы получаем файл memgraph, мы можем рассмотреть возможность использования этой команды с флагом `- summary` для вывода обзора текущей виртуальной памяти.

vmmap --summary Unity3DKit_ipx.memgraph

Вывод терминала показан ниже:

Мы можем найти кое-что интересное. Прежде всего, первые четыре столбца - это то, что мы обсуждали ранее: ВИРТУАЛЬНЫЙ РАЗМЕР, РАЗМЕР РЕЗИДЕНТА, ГРЯЗНЫЙ РАЗМЕР, ПЕРЕМЕЩЕННЫЙ РАЗМЕР.

Мы видим ВСЕГО часть. В этом игровом процессе выделяется 2,7 ГБ виртуальной памяти, из которых 1,6 ГБ сопоставлено с физической памятью, а значение DIRTY SIZE равно 1,4 ГБ - это значение очень близко к значению в отчете о памяти, а размер SWAPPED SIZE равен 52 МБ. И это значение представляет собой предварительно сжатый размер памяти, а не то, до чего она была сжата. Поэтому в основном мы обращаем внимание на пункт ГРЯЗНЫЙ РАЗМЕР.

IOKit

Во-вторых, мы видим, что у IOKit больше всего накладных расходов. Его виртуальная память не только достигает 832,5 МБ, но и размер, сопоставленный с физической памятью, достигает 750,4 МБ. Эта часть в основном представляет собой некоторую память, связанную с графическим процессором, такую ​​как цели рендеринга, текстуры, сетки, скомпилированные шейдеры и так далее.

MALLOC и куча

Опять же, мы видим, что MALLOC _ ** выделяет много памяти. Эта часть памяти в основном выделяется путем вызова Malloc, который включает в себя выделение встроенным кодом Unity C ++, а также память, выделенную сторонней библиотекой и системой с использованием Malloc. Эта память хранится в так называемой куче. Вы можете найти эти слова «см. Таблицу MALLOC ZONE ниже», то есть вы можете найти категоризацию каждой зоны кучи ниже. Здесь мы можем использовать второй инструмент командной строки `heap` для проверки содержимого памяти кучи.

heap --sortBySize Unity3DKit_ipx.memgraph

При использовании команды `heap` мы можем добавить флаг` - sortBySize` для сортировки данных по размеру, в противном случае по умолчанию выполняется сортировка по количеству экземпляров типа.

На приведенном выше рисунке вы можете видеть, что большая часть памяти кучи занята `не-объектом`, достигая почти 700 МБ, а выделение памяти объектов невелико, например, есть 573 экземпляра` GpuProgramMetal`, но они занимают только до 223кб.

Я думаю, вас может заинтересовать содержание не-объекта, но мы не можем найти дополнительную информацию на этом снимке экрана. Итак, теперь мы можем добавить флаг - showSize, чтобы сгруппировать данные по размеру.

heap --showSize --sortBySize Unity3DKit_ipx.memgraph

Это намного яснее.

Как вы можете видеть, в категории «не-объект» самые ранжированные распределения памяти - это выделение 30 МБ, три выделения по 10 МБ и выделение 8 МБ. Далее мы профилируем эти распределения памяти.

Конечно, команда `heap` также предоставляет больше функций, таких как выделение с помощью Class Name, мы можем получить адрес памяти каждого экземпляра типа через сопоставление ClassName. Просто добавьте флаг «- адреса». Например, мы можем распечатать адреса всех экземпляров GpuProgramMetal. Мы видим, что экземпляр этого класса невелик, но реальный ресурс шейдера, на который он ссылается, может иметь большие накладные расходы на память.

heap -addresses GpuProgramMetal Unity3DKit_ipx.memgraph

Имея адрес в памяти каждого объекта, мы можем узнать, откуда они взялись, с помощью команды `malloc_history`, упомянутой ниже. Но теперь мы обращаем внимание на эти относительно большие выделения памяти.

Вернемся к терминалу и распечатаем информацию о виртуальной памяти, но на этот раз мы сосредоточимся только на выделении из MALLOC_LARGE, поэтому мы можем использовать grep для фильтрации нашей цели.

vmmap -verbose Unity3DKit_ipx.memgraph | grep "MALLOC_LARGE"

Теперь мы получаем данные памяти MALLOC_LARGE, включая его адрес, размер и информацию о зоне кучи. Здесь мы можем найти наши цели: выделение 30 МБ, три выделения 10 МБ и выделение 8 МБ.

Давайте посмотрим на вызовы стека, которые их выделяют. Здесь мы будем использовать команду `malloc_history` с флагом` - fullStacks` для вывода информации о стеке.

malloc_history Unity3DKit_ipx.memgraph --fullStacks 0x0000000127c60000

Вы можете видеть, что эти 30 МБ выделены для выделения пула памяти для FMOD.

Остальные три распределения по 10 Мбайт также делают аналогичные вещи. Наконец, давайте посмотрим, откуда взялись эти 8 МБ.

malloc_history Unity3DKit_ipx.memgraph --fullStacks 0x0000000113400000

Вы можете видеть, что Unity выделяет память для создания CommandQueue, когда включен многопоточный рендеринг.

VM_ALLOC == Размер моно?

Затем мы видим, что часть результата вывода `vmmap –summary` называется` VM_ALLOC`. По словам Валентина Симонова, VM_ALLOC соответствует размеру Mono-памяти, которая является управляемой памятью. Это правда? Мы можем посмотреть на стек вызовов выделения памяти в разделе VM_ALLOC так же, как описано выше.

vmmap -verbose Unity3DKit_ipx.memgraph | grep "VM_ALLOC"

На снимке экрана показано, что эти выделения памяти относительно малы, и мы также выбираем самый большой блок для запуска.

Сначала мы используем malloc_history для профилирования части размером 3 МБ.

malloc_history Unity3DKit_ipx.memgraph --fullStacks 0x0000000152bd4000

Мы видим, что эти 3 МБ памяти выделяются методом SimplFXSynth.RenderAudio в сценарии C #, который запускает выделение сборщика мусора, а управляемая куча расширяется.

Интересно, тогда давайте посмотрим, как распределяется 1 МБ памяти.

malloc_history Unity3DKit_ipx.memgraph --fullStacks 0x0000000150084000

На этот раз это метод Unity ScriptingGCHandle :: Acquire для выделения памяти в управляемой куче.

Следовательно, память VM_ALLOC действительно соответствует управляемой куче Unity Mono. В частности, вы можете узнать, какая функция запускает выделение сборщика мусора, с помощью команды malloc_history.

Сводка команд

Теперь использование инструментов командной строки для профилирования и поиска проблем с памятью на платформе iOS завершено. Давайте сделаем простое резюме после того, как получим. Файл Memgraph из игры Unity, вы можете сначала просмотреть сводку памяти через `vmmap - summary`. Для кучи, которая представляет собой память, выделенную malloc, ее можно дополнительно проанализировать с помощью команды `heap`. Как только адрес памяти целевого объекта получен, вы можете использовать команду `malloc_history`, чтобы получить информацию о стеке вызовов для выделенной памяти. Конечно, не забудьте включить стек Malloc в Xcode. После этого вы можете создать инструмент автоматического анализа для обработки данных с целью обнаружения проблем с памятью.

0x04 «Сцена после титров»

  • Новый API iOS, os_proc_available_memory, предоставляется в iOS 13, и с помощью этого API мы можем получить оценку того, сколько памяти может получить текущий процесс. На рисунке ниже показан приблизительный объем памяти, который моя игра может использовать при запуске на iPhone7.

Полезные ссылки: