Recall Engine. Часть 1.

Recall Engine. Часть 1.

AmeliePick


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

  1. Подсистема памяти.
  2. Многопоточность.
  3. Утилиты.

Вторая часть:

  1. Подсистема ввода-вывода.
  2. Графический интерфейс.

Третья часть:

  1. 2D графика.
  2. Звуковой модуль.


Глава 1. Описание.

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

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

Движок имеет свою философию:

  1. Собственное решение работает хорошо или лучше, чем существующее и оно независимо.
    Recall полностью состоит из собственных решений. Так, например, в нём используются свои исключения вместо C++ исключений, по простой причине – C++ исключения очень медленные.
  2. Не платите за то, что не используется.
    Идея основана на модульности проекта. Нет необходимости подключать какой-то модуль, если он не используется.
  3. Производительность лучше, чем совместимость.
    Движок старается использовать и предоставлять все доступные технологии. Так, сам Recall использует 128 битные регистры, которые появились в SSE аж в 1999 году. Это означает, что использовать движок на процессорах старших Pentium III не получится... Однако, использовать его не получится на всех процессорах вплоть до Pentium 4 ревизии E0 у Intel и Athlon 64, Opteron у AMD. Recall разрабатывается строго под Window 10, которая требует наличия технологии NX bit, и которая впервые появилась в упомянутых ранее процессорах(2003-2004). Поэтому, более реальные ситуации ограничения совместимости, регулируются самим разработчиком. Решив использовать AVX512, реализованную не в каждой архитектуре, начиная с процессоров 2017 года выпуска, тем самым лишит возможности запускать ПО на более ранних, а иногда и более поздних(Alder Lake) процессорах.
  4. Работает везде, где работает ОС.
    Сама суть движка о том, что это не только инструмент для разработки игр, но ещё и обычных программ, гарантирует минимальные системные требования.


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

FAQ

О названии: Смысл названия в том, что как игровой движок, он используется для создания игр, которые, способны приносить воспоминания. Логотип же движка – это чёрная дыра. Название и логотип — это отсылка на гипотезу об исчезновении информации в чёрной дыре.

Глава 2. Подсистема памяти.

Позволяет работать с памятью(выделять, освобождать). Поддерживает размер ОЗУ до 256 ТБ, который может быть ограничен лимитами Windows:

Ограничения Windows 10 по объёму физической памяти:

Ограничения Windows на адресные пространства:

Несмотря на то, что движок разрабатывается строго под Windows 10, ядро способно запускаться и на более старых версиях системы(протестировано на Windows 7) и в таких случаях, ограничения системы будут другие.

Подсистема памяти содержит демона, который выполняет две важные функции: дефрагментация и сборка мусора. Задача менеджера памяти - эффективно осуществлять распределение памяти и быстро обрабатывать пользовательские запросы. Задача дефрагментации - перераспределение блоков памяти для обеспечения их непрерывной последовательности, что ускоряет работу с памятью. Сборка мусора в Recall не имеет ничего общего со сборщиками мусора из, например, JVM или C#. Задача сборки мусора выгружать из ОЗУ неиспользуемые участки свободной памяти, а также устранение внутренних ошибок ядра при распределении памяти. Операции демона являются пассивно-блокирующими. После истечения кванта времени не прерывают текущие операции работы с памятью, однако на время своего выполнения блокируют работу с ней.

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

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

KERNEL_BUFFER_SIZE – размер пространства ядра в байтах.

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

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

SAMPLING_TIME – время выборки в мс. Или квант времени, в течении которого будет собираться информация о потреблении памяти и прогнозироваться значение. Маленькое значение ведёт к увеличению времени выделения и потребления памяти.

RAM_SAFE_SIZE – размер памяти ОЗУ в байтах, который должен оставаться в системе. Маленькое значение может привести к зависаниям и сбоям операционной системы и других приложений, если движок выделит много памяти.

Далее представлены параметры для дефрагментации и сборки мусора. По истечению указанного времени, соответствующая операция будет выполнена:

KERNEL_DEFRAG_TIMEOUT, KERNEL_GC_TIMEOUT, USER_DEFRAG_TIMEOUT, USER_GC_TIMEOUT.

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


FAQ:

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

Можно ли отключить демон? Выполнение операций демона требует какое-то время, однако функции отключения демона не предусмотрено. Но в файле настроек можно указать большие значения интервалов, и сделать, например так, чтобы сборка мусора выполнялась раз в тысячу лет. Что не рекомедуется делать, так как снизится производительность.

Как видно из графиков, при почти одинаковом времени теста, потребление памяти вырастает почти в 1.5 раза при отключённом демоне.


Возможно ли использование стандартных функций, того же malloc, вместо методов движка? Возможно, но лишено смысла. Так как, во-первых, рано или поздно может привести к сбою приложения, а во-вторых, слишком медленно по сравнению с методом движка.

Тесты, за февраль 2022 года. Проведенны при отключённой многопоточности в подсистеме памяти, на процессоре i7 4790K на частоте 4.24 ГГц, И памятью DRR3, объёмом 8 Гб, в двухканальном режиме, на эф. частоте в 1.6 ГГц, с таймингами: 11-11-11-28.
Среднее значение - это средняя сумма значений за 5 тестов подряд.

Среднее время выделения памяти:

RE Allocator: 95 ms | malloc: 485 ms

Среднее время доступа к памяти:

RE Allocator: 30.6 ms | malloc: 176 ms

Среднее время освобождения памяти:

RE Allocator: 53.63 ms | malloc: 2535.98 ms

Далее приведены результаты для RE Allocator при усреднённом объёме выделенной памяти в 1 579 149 393 байт.

Среднее время дефрагментации: 4.8 ms.

Среднее время сборки мусора: 1.7 ms.


Без сомнений видно, насколько malloc отстаёт. В тестах блоки выделялись довольно небольшим размером, меньше одного гигабайта. Однако, на больших выделениях malloc показывает себя ещё хуже, тратя больше 100 мс на выделение памяти >= 1 Гб.

Результаты после улучшения подсистемы памяти и включённой многопоточностью в модуле:

Тесты проводились при усреднённом объёме выделенной памяти в 1 611 954 940 байт на i7 4790K с частотой в 4.49 ГГц, DDR3 памятью в 16 ГБ(2S * 4ГБ + 1D * 8 ГБ) в двухканальном режиме, с таймингами 11-11-11-28, на частоте 1.6 ГГц.

Среднее время выделения памяти: 62.8 мс (62 838 182 нс).

Среднее время освобождения памяти: 1.4 мс (1 447 459,4 нс).

Среднее время дефрагментации: 0.8 мс (896 971,6 нс).

Среднее время сборки мусора: 0.7 мс (786 384,6 нс).


Глава 3. Многопоточность.

Recall поддерживает работу с 64 физическими и 256 логическими ядрами. Поэтому, если у Вас имеется Threadripper 3990X, из него возможно выжать максимум. Так или иначе, ОС имеет свои особенности в работе с большим количеством ядер и потоков, которые подробно описаны здесь.

Вернёмся обратно к движку. Менеджер потоков имеет стандартные методы создания и управления потоками, но так же включает себя несколько дополнительных возможностей. Рассмотрим наиболее интересные:

Функция HardwareParallel позволяет перенести один из потоков на другое физическое ядро. Необходимо как минимум два физических ядра ЦП. Если второй, «распараллеленный» поток будет заблокирован(аппаратным прерыванием или APC ядра ОС), то он не будет вытеснен на другие физические ядра или приостановлен. Такой поток может продолжить выполнение в другом потоке выполнения данного физического ядра.

Это не значит, что любые два других потока не будут выполнятся параллельно, не используя данную функцию. Если в BIOS включена функция многопоточности, то любые два потока, выполняющиеся на двух логических ядрах одного физического ядра, будут выполнены параллельно.

С точки зрения Recall, два потока на одном ядре находятся в состоянии конкуренции, хотя фактически они могут выполнятся параллельно. Любые два потока находятся в состоянии параллелизма, когда они выполняются на разных физических ядрах.

Task/Задача

Аналог стандартного потока, но предназначен для более коротких по времени операций и за счёт этого, издержки на его создание меньше, чем при создании обычного потока: <1 мс против ≈6 мс.

Семафор

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

FAQ - ¯\_(ツ)_/¯



Глава 4. Утилиты.

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

Лямбда

Движок располагает тремя видами лямбда функций:

Статичная – информация о ней выводится на моменте компиляции.

Динамическая – информация о ней выводится во время выполнения программы.

Динамическая типизированная – имеет определённые типы возврата и аргументов.

Лямбды являются частью IFC(inter-function communication) движка – низкоуровневый код, позволяющий более гибко работать с функциями. Например, IFC позволяет использовать метод класса в виде функции обратного вызова, избегать C++ преобразований над указателями функций, используя адрес функции как void*(Вы знаете, что делаете, если используете это) и многое другое.

Контейнеры

На момент написания статьи, Recall реализовывает два контейнера: словарь и список. Если словарь мало чем отличается от стандартных решений, то вот со списком есть ряд моментов. Список является нетипизированным контейнером. Это значит, что разработчик в один список может добавлять объекты разных типов. Единственным параметром списка является аллокатор – объекты будут храниться либо в ядре, либо в пользовательском пространстве. Данное решение хоть и имеет свои преимущества, однако не лишено недостатков. Так, к минусам можно отнести тот факт, что при удалении списка, разработчик должен вручную вызвать деструкторы объектов. Recall также предоставляет обёртку для данного списка, которая позволяет использовать уже привычный, типизированный список.

Строка

Суть данной реализации строки в том, что она позволяет разработчику решать, как именно в памяти будет располагаться строка: по адресу A будет располагаться и класс строки и сами символы, либо по адресу А будет только класс строки, а символы будут по С адресу. Понятно, что строка пытается максимально использовать подсистему памяти. Помимо этого, она имеет большое количество методов, что позволяет работать со строкой без «боли», как будто это не C++, а более высокоуровневый язык.

Кроме строки, движок предоставляет типы для работы с SIMD, временем, потоками т.д.

FAQ - ¯\_(ツ)_/¯


Заключение

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

Программа довольно проста, по простой причине - придумать такую, чтобы охватывала вообще все возможности движка, требует больше времени, чем написание всех трёх статей ( > ‿ < )

Код можно найти по ссылке, который на Телеграфе не очень хорошо отображается.

Пояснения:

Сперва инициализируется ядро движка. Затем создаётся нетипизированный список и заполняется значениями. Далее объявляется лямбда функция, принимающая в качестве параметра, указатель на структукру Args, содержащую два числа: начало и конец диапазона для сортировки.

Эта лямбда передаётся в LambdaWrapper - динамическую типизированную лямбду, которая используется при создании задачи.

Вообще, функция Create может иметь и следующий вид:

RETask::Create(LambdaWrapper<void, void>([]()
{
  // Do something.
}), &arg, true);

Функции Create передаётся также указатель на аргументы, и третьим параметром указывается автостарт текущей задачи.

Пока задача выполняется в другом потоке, основной поток изменяет значения второй части списка, а затем дожидается пока задача не выполнится, после которой сам вызывает лямбду sort.

В результате должен получиться примерно следующий вывод:

1 7 4 0 9 4 8 8 2 4
0 1 4 7 9 66 69 102 59 10
0 1 4 7 9 10 59 66 69 102


Дорожная карта и описание проекта можно найти тут.

Конец.




Report Page