
Дневник разработчиков. Часть 1
Старт серии дневников разработчиков
Мы запускаем серию дневников разработчиков, в которых будем рассказывать о текущей работе над новым функционалом движка, публиковать статьи о языке Daslang и сравнивать EdenSpark с конкурентами. Приятного чтения!
Дисклеймер
Сразу уточню, что данный дневник разработчиков содержит лишь частичное сравнение производительности EdenSpark и языка Daslang с Unity и языка C#.
Сравнение будет осуществленно на синтетических тестах, что не полностью отражает картину с реальной игровой нагрузкой. И непосредственно я, как автор данного дневника, не имею коммерческого опыта работы с Unity и могу упускать какие-то аспекты оптимизации Unity и C#.
Описание бенчмарка
В качестве бенчмарка был взят следующий стресс-тест для проверки эффективности дерева иерархии:
- Каждый кадр каждая нода дерева может создать еще одну дочернюю ноду, пока глубина не превысит 7 или общее число нод не превысит 16000
- Каждый объект использует свой уникальный цвет в зависимости от глубины в иерархии (черный у корня, желтый у листьев)
Данный стресс-тест проверяет следующий функционал игрового движка:
- Создание новых нод
- Работа с особенно ветвистым деревом иерархии
- Рендер большого числа однотипных объектов, но с разными per instance data.

Параметры железа и софта
И Unity и EdenSpark будет использовать DX12 в качестве графического драйвера.
Используется Unity 6.2.
CPU: AMD Ryzen 7 6800H with Radeon Graphics
CPU: AMD Ryzen 7 6800H with Radeon Graphics
Base speed: 3,20 GHz Sockets: 1 Cores: 8 Logical processors: 16 Virtualization: Enabled L1 cache: 512 KB L2 cache: 4,0 MB L3 cache: 16,0 MB
Utilization: 7% Speed: 2,81 GHz Up time: 2:03:13:04 Processes: 299 Threads: 5500 Handles: 141949
GPU: NVIDIA GeForce RTX 3060 Laptop GPU
GPU: NVIDIA GeForce RTX 3060 Laptop GPU
Driver version: 31.0.15.4692 Driver date: 30.04.2024 DirectX version: 12 (FL 12.2) Physical location: PCI bus 1, device 0, function 0
Utilization: 2% Dedicated GPU memory: 2,1/6,0 GB Shared GPU memory: 0,3/15,6 GB GPU Memory: 2,4/21,6 GB
Замеры производительности EdenSpark


В качестве точки интереса будем исследовать момент времени, когда создаются последние ноды, этот момент отчетливо заметен в профайлере, после достижения этой точки нагрузка на CPU значительно уменьшается. Зафиксируем ключевые метрики:
- Обновление пользовательских компонент и кода в отдельном треде занимает 19.66 мс
- Обновление всех объектов (act) и диспатч команд отрисовки 2.88 мс
- Непосредственно отрисовка на GPU в сумме 8.94 мс
Возьмем эти данные за точку отсчета и посмотрим что будет в Unity Engine!
Если вам интересно поизучать профайл самостоятельно, то вы можете скачать daProfiler здесь: https://github.com/GaijinEntertainment/daProfiler.
И непосредственно сохраненный профайл по кнопке ниже
Unity Engine
Поскольку я не имею большого опыта работы с Unity Engine я попросил Copilot переписать код нашего стресс-теста с языка Daslang на Unity C#.
Очень важно отметить, что я взял шаблон с BuiltIn рендер пайплайном, это может значительно повлиять на результаты измерений! Получилось перенести код без ошибок компиляции и с минимум дополнительных действий. Пришлось только выключить Input System Package и сделать парочку нод для сферы, пола и текста.
Единственное, что пришлось менять, чтобы тест выглядел одинаково это approach метод, также известный как экспоненциальный Lerp. Copilot сгенерировал следующий код для этой функции, но MoveTowards это вероятно обычный Lerp:
Vector3 Approach(Vector3 current, Vector3 target, float deltaTime, float speed)
{
return Vector3.MoveTowards(current, target, speed * deltaTime);
}Я заменил его на следующий и Unity стала выдавать тот же самый результат (визуально), что и EdenSpark
Vector3 Approach(Vector3 current, Vector3 target, float deltaTime, float speed)
{
return Vector3.Lerp(current, target, 1.0f - Mathf.Exp(-Mathf.Log(2) * deltaTime / 0.5f));
}
Профилируем Unity

Unity показывает следующий перф:
- Обновление пользовательского кода 52.66 мс (В 2.68 раз медленнее Eden'а)
- CPU часть отрисовки 126-127 мс (в Main/Render thread) (В 44 раза медленнее Eden'a)
- GPU - 147 мс (В 16,4 раз медленнее Eden'a)
Оптимизация Unity
Достаточно быстро стало понятно, что Unity не смог задействовать instancing объектов. Мое внимание привлек этот кусок кода, прошу Copilot его исправить.
Renderer renderer = node.GetComponent<Renderer>();
if (renderer != null)
{
Material mat = new Material(renderer.material);
mat.color = color;
renderer.material = mat;
}Мне был предложен следующий подход на основе MaterialPropertyBlock. Мне он понравился и я заменил необходимые куски кода, а также поменял галку в материале "Enable GPU Instancing".
public class TreePerformanceTest : MonoBehaviour
{
[Header("Optimization")]
public Material sharedMaterial; // One material for sharing
private MaterialPropertyBlock materialPropertyBlock;
private static readonly int ColorPropertyID = Shader.PropertyToID("_Color");
void Awake()
{
// Create one MaterialPropertyBlock for reusing
materialPropertyBlock = new MaterialPropertyBlock();
InitializeInputActions();
}
GameObject AddObject(float scale, Vector3 position, Color color, Transform parent = null)
{
GameObject node = Instantiate(spherePrefab);
node.transform.position = position;
node.transform.localScale = Vector3.one * scale;
if (parent != null)
{
node.transform.SetParent(parent);
}
Renderer renderer = node.GetComponent<Renderer>();
if (renderer != null && sharedMaterial != null)
{
// Use shared material
renderer.material = sharedMaterial;
// Set actual color in MaterialPropertyBlock
materialPropertyBlock.SetColor(ColorPropertyID, color);
renderer.SetPropertyBlock(materialPropertyBlock);
}
return node;
}
}Профилируем Unity с включенным инстансингом

Обновленные замеры:
- Обновление пользовательского кода 44.38 мс, а было только что 52.66 мс. Возможно ошибка замеров, или просто при низком фпс замеры производительности не стабильны.
- CPU часть отрисовки 50 мс (Main thread) - 74 мс (Render thread) против 127 мс ранее
- GPU - 67.8 мс против 147 мс ранее.
Видим, что отрисовка стала в два раза быстрее, но общий фпс остается все еще очень низким. Однако профилирование GPU, согласно Unity, значительно нагружает CPU и делает CPU профайл невалидным! Давайте посмотрим, что будет если выключить сбор GPU данных.
Новый профайл уже показывает значительно лучшие результаты:

Обновленные замеры:
- Обновление пользовательского кода 45.94 мс, в прошлом профайле 44.38 мс. Видим что этот код выполняется достаточно стабильно
- CPU часть отрисовки 21.12 мс (Main thread) - 21.84 мс (Render thread) против 50 мс и 74 мс с GPU профилированием
- GPU - неизвестно
Кажется, что теперь мы профилируем что-то адекватное.
Я бы очень хотел сделать вывод, что CPU часть рендера в EdenSpark в 7.3 раз быстрее, чем в Unity (21.12 мс / 2.88 мс), но лучше сделать сравнение нашего рендера с Universal Render Pipeline. Так что оставлю сравнение рендера для следующих частей дневника. А сейчас сконцентрируюсь на сравнении пользовательского кода.
Unity Release Build
Известно, что Unity редактор добавляет значительный оверхед на выполнение пользовательского кода. Поэтому для чистоты эксперимента соберем релизную версию нашего стресс-теста. Оставим Dev Build галку включенной, чтобы подключаться профайлером.

После успешного билда я сделал замеры и убрал все не относящееся к пользовательскому коду, чтобы не засорять экран:

Получаем 38.8 мс в Release Unity против 19.66 мс в EdenSpark.
Если присмотреться, то каждый фрейм делится на 2 части:
- обновление дерева - рекурсивный обход большого дерева с созданием большого числа нод
- обновление позиции каждого объекта через его on_update/OnUpdate коллбек
В Eden (кадр 19.66 мс):
- обновление дерева - 16.8 мс (85% от кадра)
- обновление позиций нод - 1.9 мс (9.6% от кадра)
- отправка актуальных трансформов объектов в render тред 0.8 мс. Но мы не будем сравнивать эту рендер related часть cpu работы.
В Unity (кадр 38.8 мс в 1.97 раза медленнее Eden'а)
- обновление дерева - 30.7 мс (79% от кадра). В 1.82 раза медленнее Eden'а
- обновление позиций нод - 8 мс (20.6% от кадра). В 4.21 раза медленнее Eden'а
Unity AoT(IL2CPP) Release Build
Но давайте дадим еще больше форы Unity и воспользуемся IL2CPP, чтобы сделать aot (ahead of time) компиляцию C# в С++.
EdenSpark выполняет Daslang код в интерпретации (JiT PC-only, AoT компиляция доступна в standalone сборке EdenSpark), так что мы даем значительную фору Unity!


Обычный Release в Unity 38.8 мс (в 1.97 раза медленнее Eden'а)
IL2CPP Release в Unity 36.6 мс (в 1.86 раза медленнее Eden'а)
Итог
Отрисовка большого числа однотипных объектов (16к сфер) дается Unity с трудом. Скорость работы BuiltIn render pipeline на стороне CPU в 7.3 раза медленнее по сравнению с той же работой у EdenSpark.
Выполнение таких вещей как инстанциирование объектов, работа связанная с итерацией по иерархии сцены, вызов OnUpdate и изменение трансформов нод в Unity получается в 1.86 раза медленнее .
В EdenSpark используется язык Daslang в режиме интерпретации и он оказывается практически в 2 раза быстрее C# в AoT исполнении.