Дневник разработчиков Victoria 3 №76. Производительность

Привет и добро пожаловать в дневник разработчиков этой недели по Victoria 3. Сегодня мы немного поговорим о производительности и о том, как игра работает изнутри. Местами мы углубимся в подробности, поэтому, если вас интересуют лишь улучшения в версии 1.2, их вы можете найти ближе к концу.
Если кто-то не знает, меня зовут Эмиль, и я работаю в Paradox с 2018 года. Я присоединился к команде Victoria 3 в качестве технического руководителя в 2020-м, а до этого занимал ту же должность в других проектах.
Что такое производительность
Трудно заводить разговор о производительности, не обозначив сперва, что под этим подразумевается. Во многих играх это сводится к максимальной частоте кадров в секунду (FPS) без слишком сильного снижения графических настроек. Но в играх со множеством симулируемых систем, какие мы создаём в PDS, на первый план выходит другой аспект: скорость тактов (или расчётов) в игре. У этого параметра нет столь широко известного и устоявшегося названия, как у частоты кадров, но в других играх вы могли встречать названия вроде «тактов в секунду» или «обновлений в секунду». Здесь я буду использовать противоположный параметр: сколько времени в секундах или миллисекундах уходит на обработку одного такта на среднем компьютере. Часть графиков будет из отладочных версий игры, а часть из релизных версий, так что числа не всегда буду прямо сопоставимы.
Определение такта может отличаться от игры к игре. В CK3 и EU4 такт составляет один день, тогда как в HOI4 это всего час. В Victoria 3 один такт составляет шесть часов, то есть четверть дня. Но не все такты равны. Некоторые расчёты нужно выполнять реже других, поэтому мы делим такты на категории. В Victoria 3 у нас ежегодные, ежемесячные, еженедельные, ежедневные и (обычные) такты.

Версия 1.1 казалась медленной? Вы бы видели игру за год до выхода…
Содержимое такта
Victoria 3 полна различных симуляций, поэтому в каждом такте нужно проводить очень много вычислений. Чтобы сохранить порядок в коде, мы разбили каждый такт на отдельные задачи. Тактовая задача — это набор операций, которые необходимо провести над игрой, а также информация о том, как часто их нужно проводить и какие тактовые задачи нужно выполнить прежде, чем запускать эту.

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

Десять самых ресурсозатратных тактовых задач по данным нашего ночного теста от 15-го февраля. Числа в секундах — это усреднённый показатель по нескольким запускам отладочной версии игры.
Как видно из графика выше, большинство самых ресурсозатратных задач выполняются еженедельно. А учитывая, что в еженедельный такт помимо этого входят все ежедневные и обычные задачи, на его обработку может уйти немало времени. Давайте рассмотрим подробнее, что же происходит внутри еженедельного такта. Для этого мы можем использовать профайлер. Помимо прочих мы в PDS используем профайлер Optick — у него открытый исходный код и он предназначен в основном для разработки игр.

Данные Optick для еженедельного такта в районе 1890 года в релизной версии игры.
На скриншоте выше очень много всего, так что давайте разберём его подробнее. Слева указано название потоков, которые мы просматриваем. Сначала идёт поток Сеть/сессия (Network/Session) — главный поток в логике игры. Он отвечает за работу симуляции и приём команд от игрока. Затем у нас идут потоки основных задач (primary task). Их количество может меняться на разных компьютерах, поскольку движок создаёт разное число потоков в зависимости от числа ядер процессора. Для скриншота я вручную ограничил это число восемью, чтобы нам было проще. Потоки задач отвечают за работу, которую можно выполнять параллельно. Дальше идёт главный поток (Main Thread). Это первоначальный поток, созданный операционной системой при запуске игры, и он отвечает за обработку обновлений интерфейса и графики. Затем поток отрисовки (Render Thread), благодаря которому отрисовывается сама игры. И наконец, потоки второстепенных задач. Они схожи с потоками основных задач, но в основном отвечают за неигровую логику: помощь в обновлении графики или сохранение игры.
Разноцветные блоки с текстом — это различные части кода, которые мы сочли достаточно интересными, чтобы отображать их в профайлере. Мы могли бы использовать другой профайлер для получения более подробных данных, например Superluminal или VTune, которые позволили бы спуститься до уровня функций или даже машинного кода.
Розовые полосы означают, что поток чего-то ждёт. В случае с потоками задач это обычно означает, что они уже выполнили всю работу и ждут новой, тогда как в случае с потоком сессии это чаще сигнализирует о том, что он пока не может менять состояние игры, потому что для обновления интерфейса или графики понадобились данные из него.
При изучении скорости тактов нас в основном интересуют потоки сессии и основных задач. На скриншоте я развернул поток сессии, чтобы мы увидели, что происходит внутри еженедельного такта. Кое-что тут выделяется.
Для начала, у нас частенько встречаются красные блоки CScopedGameStateRelease. Это время, когда мы делаем перерыв в обновлении, чтобы интерфейс и графика могли прочесть данные, нужные им для продолжения отрисовки игры с частотой, максимально близкой к 60 кадрам в секунду. Но эти блоки не могут появляться где попало — только между тактовыми задачами или между определёнными шагами внутри этих задач. Это необходимо для обеспечения достоверности данных, например, чтобы интерфейс не считывал данные, когда обновилась только половина бюджета страны.
Ещё на скриншоте выделяется тактовая задача UpdateEmployment, как и на графике выше. Но тут мы можем получить больше о ней узнать. С первого взгляда можно заметить, что она разделена на (как минимум) две части: параллельную и последовательную. В идеале мы все вычисления хотим выполнять параллельно, потому что это позволяет лучше задействовать мощности современных процессоров. К сожалению, не всё, что касается занятости населения, можно вычислить параллельно, потому что приходится обращаться к глобальным операциям, вроде создания и уничтожения объектов населения и выполнения скриптов. Так что мы выделили как можно большую часть в предварительные параллельные расчёты, чтобы максимально снизить время выполнения последовательной части. Хотя на самом деле есть ещё третья часть между ними, но её на скриншоте не видно, потому что она слишком быстро выполняется. На этом этапе выполняется сортировка, чтобы предотвратить рассинхроны в сетевой игре из-за порядка поступления данных из параллельных потоков.

Подробности о тактовой задаче UpdateEmployment.
Модификаторы медленные
Общим понятием среди всех игр PDS являются модификаторы, и Victoria 3 не исключение — скорее наоборот. По сравнению с CK3 наши модификаторы на порядок сложнее. Для их обработки мы используем систему, схожую с системой Stellaris, которую мы называет узлами модификаторов. Вкратце, это система управления зависимостями, которая позволяет помечать модификаторы и пересчитывать только их и зависящие от них модификаторы. Это довольно полезно, поскольку пересчёт модификатора — дело ресурсоёмкое.
Однако большинство вычислений в рамках этой системы выполнялись в одном потоке, а это значит, что большая часть нашего такта тратилась на обновление модификаторов. Взглянув на график в начале дневника, вы заметите, что производительность сильно улучшилась в начале 2022 года. Одной из причин этого был переход на параллельный подсчётов узлов модификаторов. Поскольку мы знаем о зависимостях узлов, мы можем разделить узлы на группы так, чтобы каждая группа зависела только от предыдущих.

Подробности о тактовой задаче RecalculateModifierNodes
Страны от мала до велика
Большая часть работы в рамках такта выполняется для каждой страны в мире. Но из-за огромной разницы в размере между небольшой страной, такой как Люксембург, и крупной, как Россия, некоторые операции для одной страны могут выполняться в сто раз медленнее, чем для другой. При последовательном вычислении это не имеет особого значения, так как надо выполнить всю работу, и неважно, какую часть закончить первой. Но при параллельном выполнении мы можем столкнуться с проблемой, когда слишком много крупных стран оказываются в одном потоке. Это значит, что когда все потоки завершат работу, нам всё равно придётся ждать последний поток. Чтобы избежать этого, мы разработали систему, в которой тактовые задачи могут указывать эвристическую стоимость для каждой части обновления. Это в свою очередь позволяет нам определять выделяющиеся части, взглянув на стандартное отклонение рассчётного времени вычислений, и планировать их отдельно.
Это имеет большое значение, например, в изменении бюджета страны. Распределив Китай, Россию и Великобританию по разным потокам, мы значительно сокращаем время для обновления бюджета.
Кстати, именно по этой причине игра работает медленнее в кампаниях по завоеванию мира!

Подробности о тактовой задаче WeeklyCountryBudgetUpdateParallel. Обратите внимание на дорогие и доступные (Expensive и Affordable).
Изменения в 1.2
Осмелюсь предположить, что эту часть вы ждали больше всего. Мы внесли множество как мелких, так и крупных изменений.
Если вы следили за изменениями в открытой бете, то могли заметить некоторые измения интерфейса в очереди строительства. Учитывая стиль игры многих игроков, очередь у них могла становиться очень большой. К сожалению, старому интерфейсу требовалось вычислять размер всех своих элементов, чтобы расположить их должным образом. Включая те элементы, которые не видно на экране.

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

Обновление графики до и после оптимизации.
Как уже говорилось выше, обновление занятости населения значительно влияет на производительность. И это очень тесно связано с ростом числа единиц населения. То есть количества объектов, а не самого населения. Вы могли столкнуться с большим количеством крошечных единиц населения, особенно в поздней игре, которые значительно замедляют обновление занятости. Для борьбы с этим мы изменили то, насколько активно игра объединяет маленькие единицы населения, что должно улучшить производительность игры на поздней стадии. Мододелы могут изменить значения в defines: POP_MERGE_MAX_WORKFORCE и POP_MERGE_MIN_NUM_POPS_SAME_PROFESSION.
Ещё одно улучшение, которое мы сделали в версии 1.2 — это изменили способ распределения памяти в Clausewitz. Хотя у нас всегда были отдельные распределители для особых случаев (распределители пула, игровой объект databases и т. д.), всё ещё оставалось множество распределений, которые упирались в распределитель по умолчанию, опиравшийся на операционную систему. Это могло работать чрезвычайно медленно, особенно на Windows. Для решения этой проблемы мы решили использовать библотеку под названием mimalloc. Это очень эффективный распределитель памяти и, фактически, замена функционалу операционной системы. Эта библотека уже используется другими крупными движками, например Unreal Engine. И хотя это не такое значительное изменение, как упомянутые ранее, но оно ускорило работу игры примерно на 4%, если измерять за год в районе двух третей временной шкалы. Ну и, поскольку это улучшение движка, вероятно, в будущем вы увидите его и в CK3.
В дополнение к этим крупным измениям мы также внесли множество небольших улучшений, которые в совокупности оказывают значительный эффект. В целом игра на версии 1.2 работает намного быстрее, чем на 1.1, как вы можете увидеть на графике ниже. К сожалению, ночные тесты версии 1.1 были менее стабильными, чем 1.2, поэтому для наглядности я обрезал график на 1871 году, но в целом улучшения производительности в 1.2 ещё более заметны в поздней стадии игры.

Погодовое сравнение времени тактов между версиями 1.1 и 1.2 — последняя намного быстрее. Числа — это средние годовые значения из нескольких ночных тестов на протяжении нескольких недель на отладочных версиях игры.
У меня на этой неделе всё. На следующей Ник покажет разные улучшения в механике войн в версии 1.2, включая новую возможность «Стратегические цели».
Для просмотра ссылки Зарегистрируйтесь

Привет и добро пожаловать в дневник разработчиков этой недели по Victoria 3. Сегодня мы немного поговорим о производительности и о том, как игра работает изнутри. Местами мы углубимся в подробности, поэтому, если вас интересуют лишь улучшения в версии 1.2, их вы можете найти ближе к концу.
Если кто-то не знает, меня зовут Эмиль, и я работаю в Paradox с 2018 года. Я присоединился к команде Victoria 3 в качестве технического руководителя в 2020-м, а до этого занимал ту же должность в других проектах.
Что такое производительность
Трудно заводить разговор о производительности, не обозначив сперва, что под этим подразумевается. Во многих играх это сводится к максимальной частоте кадров в секунду (FPS) без слишком сильного снижения графических настроек. Но в играх со множеством симулируемых систем, какие мы создаём в PDS, на первый план выходит другой аспект: скорость тактов (или расчётов) в игре. У этого параметра нет столь широко известного и устоявшегося названия, как у частоты кадров, но в других играх вы могли встречать названия вроде «тактов в секунду» или «обновлений в секунду». Здесь я буду использовать противоположный параметр: сколько времени в секундах или миллисекундах уходит на обработку одного такта на среднем компьютере. Часть графиков будет из отладочных версий игры, а часть из релизных версий, так что числа не всегда буду прямо сопоставимы.
Определение такта может отличаться от игры к игре. В CK3 и EU4 такт составляет один день, тогда как в HOI4 это всего час. В Victoria 3 один такт составляет шесть часов, то есть четверть дня. Но не все такты равны. Некоторые расчёты нужно выполнять реже других, поэтому мы делим такты на категории. В Victoria 3 у нас ежегодные, ежемесячные, еженедельные, ежедневные и (обычные) такты.

Версия 1.1 казалась медленной? Вы бы видели игру за год до выхода…
Содержимое такта
Victoria 3 полна различных симуляций, поэтому в каждом такте нужно проводить очень много вычислений. Чтобы сохранить порядок в коде, мы разбили каждый такт на отдельные задачи. Тактовая задача — это набор операций, которые необходимо провести над игрой, а также информация о том, как часто их нужно проводить и какие тактовые задачи нужно выполнить прежде, чем запускать эту.

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

Десять самых ресурсозатратных тактовых задач по данным нашего ночного теста от 15-го февраля. Числа в секундах — это усреднённый показатель по нескольким запускам отладочной версии игры.
Как видно из графика выше, большинство самых ресурсозатратных задач выполняются еженедельно. А учитывая, что в еженедельный такт помимо этого входят все ежедневные и обычные задачи, на его обработку может уйти немало времени. Давайте рассмотрим подробнее, что же происходит внутри еженедельного такта. Для этого мы можем использовать профайлер. Помимо прочих мы в PDS используем профайлер Optick — у него открытый исходный код и он предназначен в основном для разработки игр.

Данные Optick для еженедельного такта в районе 1890 года в релизной версии игры.
На скриншоте выше очень много всего, так что давайте разберём его подробнее. Слева указано название потоков, которые мы просматриваем. Сначала идёт поток Сеть/сессия (Network/Session) — главный поток в логике игры. Он отвечает за работу симуляции и приём команд от игрока. Затем у нас идут потоки основных задач (primary task). Их количество может меняться на разных компьютерах, поскольку движок создаёт разное число потоков в зависимости от числа ядер процессора. Для скриншота я вручную ограничил это число восемью, чтобы нам было проще. Потоки задач отвечают за работу, которую можно выполнять параллельно. Дальше идёт главный поток (Main Thread). Это первоначальный поток, созданный операционной системой при запуске игры, и он отвечает за обработку обновлений интерфейса и графики. Затем поток отрисовки (Render Thread), благодаря которому отрисовывается сама игры. И наконец, потоки второстепенных задач. Они схожи с потоками основных задач, но в основном отвечают за неигровую логику: помощь в обновлении графики или сохранение игры.
Разноцветные блоки с текстом — это различные части кода, которые мы сочли достаточно интересными, чтобы отображать их в профайлере. Мы могли бы использовать другой профайлер для получения более подробных данных, например Superluminal или VTune, которые позволили бы спуститься до уровня функций или даже машинного кода.
Розовые полосы означают, что поток чего-то ждёт. В случае с потоками задач это обычно означает, что они уже выполнили всю работу и ждут новой, тогда как в случае с потоком сессии это чаще сигнализирует о том, что он пока не может менять состояние игры, потому что для обновления интерфейса или графики понадобились данные из него.
При изучении скорости тактов нас в основном интересуют потоки сессии и основных задач. На скриншоте я развернул поток сессии, чтобы мы увидели, что происходит внутри еженедельного такта. Кое-что тут выделяется.
Для начала, у нас частенько встречаются красные блоки CScopedGameStateRelease. Это время, когда мы делаем перерыв в обновлении, чтобы интерфейс и графика могли прочесть данные, нужные им для продолжения отрисовки игры с частотой, максимально близкой к 60 кадрам в секунду. Но эти блоки не могут появляться где попало — только между тактовыми задачами или между определёнными шагами внутри этих задач. Это необходимо для обеспечения достоверности данных, например, чтобы интерфейс не считывал данные, когда обновилась только половина бюджета страны.
Ещё на скриншоте выделяется тактовая задача UpdateEmployment, как и на графике выше. Но тут мы можем получить больше о ней узнать. С первого взгляда можно заметить, что она разделена на (как минимум) две части: параллельную и последовательную. В идеале мы все вычисления хотим выполнять параллельно, потому что это позволяет лучше задействовать мощности современных процессоров. К сожалению, не всё, что касается занятости населения, можно вычислить параллельно, потому что приходится обращаться к глобальным операциям, вроде создания и уничтожения объектов населения и выполнения скриптов. Так что мы выделили как можно большую часть в предварительные параллельные расчёты, чтобы максимально снизить время выполнения последовательной части. Хотя на самом деле есть ещё третья часть между ними, но её на скриншоте не видно, потому что она слишком быстро выполняется. На этом этапе выполняется сортировка, чтобы предотвратить рассинхроны в сетевой игре из-за порядка поступления данных из параллельных потоков.

Подробности о тактовой задаче UpdateEmployment.
Модификаторы медленные
Общим понятием среди всех игр PDS являются модификаторы, и Victoria 3 не исключение — скорее наоборот. По сравнению с CK3 наши модификаторы на порядок сложнее. Для их обработки мы используем систему, схожую с системой Stellaris, которую мы называет узлами модификаторов. Вкратце, это система управления зависимостями, которая позволяет помечать модификаторы и пересчитывать только их и зависящие от них модификаторы. Это довольно полезно, поскольку пересчёт модификатора — дело ресурсоёмкое.
Однако большинство вычислений в рамках этой системы выполнялись в одном потоке, а это значит, что большая часть нашего такта тратилась на обновление модификаторов. Взглянув на график в начале дневника, вы заметите, что производительность сильно улучшилась в начале 2022 года. Одной из причин этого был переход на параллельный подсчётов узлов модификаторов. Поскольку мы знаем о зависимостях узлов, мы можем разделить узлы на группы так, чтобы каждая группа зависела только от предыдущих.

Подробности о тактовой задаче RecalculateModifierNodes
Страны от мала до велика
Большая часть работы в рамках такта выполняется для каждой страны в мире. Но из-за огромной разницы в размере между небольшой страной, такой как Люксембург, и крупной, как Россия, некоторые операции для одной страны могут выполняться в сто раз медленнее, чем для другой. При последовательном вычислении это не имеет особого значения, так как надо выполнить всю работу, и неважно, какую часть закончить первой. Но при параллельном выполнении мы можем столкнуться с проблемой, когда слишком много крупных стран оказываются в одном потоке. Это значит, что когда все потоки завершат работу, нам всё равно придётся ждать последний поток. Чтобы избежать этого, мы разработали систему, в которой тактовые задачи могут указывать эвристическую стоимость для каждой части обновления. Это в свою очередь позволяет нам определять выделяющиеся части, взглянув на стандартное отклонение рассчётного времени вычислений, и планировать их отдельно.
Это имеет большое значение, например, в изменении бюджета страны. Распределив Китай, Россию и Великобританию по разным потокам, мы значительно сокращаем время для обновления бюджета.
Кстати, именно по этой причине игра работает медленнее в кампаниях по завоеванию мира!

Подробности о тактовой задаче WeeklyCountryBudgetUpdateParallel. Обратите внимание на дорогие и доступные (Expensive и Affordable).
Изменения в 1.2
Осмелюсь предположить, что эту часть вы ждали больше всего. Мы внесли множество как мелких, так и крупных изменений.
Если вы следили за изменениями в открытой бете, то могли заметить некоторые измения интерфейса в очереди строительства. Учитывая стиль игры многих игроков, очередь у них могла становиться очень большой. К сожалению, старому интерфейсу требовалось вычислять размер всех своих элементов, чтобы расположить их должным образом. Включая те элементы, которые не видно на экране.

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

Обновление графики до и после оптимизации.
Как уже говорилось выше, обновление занятости населения значительно влияет на производительность. И это очень тесно связано с ростом числа единиц населения. То есть количества объектов, а не самого населения. Вы могли столкнуться с большим количеством крошечных единиц населения, особенно в поздней игре, которые значительно замедляют обновление занятости. Для борьбы с этим мы изменили то, насколько активно игра объединяет маленькие единицы населения, что должно улучшить производительность игры на поздней стадии. Мододелы могут изменить значения в defines: POP_MERGE_MAX_WORKFORCE и POP_MERGE_MIN_NUM_POPS_SAME_PROFESSION.
Ещё одно улучшение, которое мы сделали в версии 1.2 — это изменили способ распределения памяти в Clausewitz. Хотя у нас всегда были отдельные распределители для особых случаев (распределители пула, игровой объект databases и т. д.), всё ещё оставалось множество распределений, которые упирались в распределитель по умолчанию, опиравшийся на операционную систему. Это могло работать чрезвычайно медленно, особенно на Windows. Для решения этой проблемы мы решили использовать библотеку под названием mimalloc. Это очень эффективный распределитель памяти и, фактически, замена функционалу операционной системы. Эта библотека уже используется другими крупными движками, например Unreal Engine. И хотя это не такое значительное изменение, как упомянутые ранее, но оно ускорило работу игры примерно на 4%, если измерять за год в районе двух третей временной шкалы. Ну и, поскольку это улучшение движка, вероятно, в будущем вы увидите его и в CK3.
В дополнение к этим крупным измениям мы также внесли множество небольших улучшений, которые в совокупности оказывают значительный эффект. В целом игра на версии 1.2 работает намного быстрее, чем на 1.1, как вы можете увидеть на графике ниже. К сожалению, ночные тесты версии 1.1 были менее стабильными, чем 1.2, поэтому для наглядности я обрезал график на 1871 году, но в целом улучшения производительности в 1.2 ещё более заметны в поздней стадии игры.

Погодовое сравнение времени тактов между версиями 1.1 и 1.2 — последняя намного быстрее. Числа — это средние годовые значения из нескольких ночных тестов на протяжении нескольких недель на отладочных версиях игры.
У меня на этой неделе всё. На следующей Ник покажет разные улучшения в механике войн в версии 1.2, включая новую возможность «Стратегические цели».
Для просмотра ссылки Зарегистрируйтесь