Улучшение производительности поиска Burst Inspector

Меня зовут Йонас Рехольт, и я студент, работающий в команде Burst. Я пишу в блоге, чтобы поделиться своим опытом оптимизации, который помог сделать возможными недавние изменения в производительности Burst Inspector. Поиск в Burst Inspector теперь работает в 13 раз быстрее, позволяя разработчикам быстрее сосредоточиться на коде, который им важен при оптимизации проектов.
Продолжайте читать, чтобы узнать, как Вы можете использовать Unity Profiler для исследования узких мест в производительности Вашей программы и как их устранить.
Компилятор Unity Burst преобразует Ваш код на C# в высокооптимизированный код на ассемблере. Burst Inspector позволяет Вам проверять код сборки прямо в редакторе Unity, поэтому Вам не нужно использовать внешние инструменты для простой проверки кода.
Когда Вы впервые откроете Burst Inspector и выберете целевое задание для отображения, Вы увидите окно, подобное изображенному ниже.

Как Вы можете видеть, Burst Inspector обеспечивает подсветку синтаксиса, стрелки ветвей и многое другое.
Инспектор попытается прокрутить страницу до сборки, которая реализует выбранную целевую функцию, но также полезно поискать в представлении сборки конкретные инструкции, комментарии и т.д. Это подводит нас к теме этой статьи в блоге.
Чтобы выполнить поиск, инспектор должен найти исходный вывод сборки и преобразовать эти индексы в позиции в представлении инспектора. Оригинальная функциональность поиска следовала схеме, показанной ниже, и в значительной степени опиралась на реализацию System.String.IndexOf(*).
while (assemblyCode.IndexOf(key, accIdx) >= 0) {
// ...
// Do logic for handling search hits
// ...
}
Выполнение вышеуказанного поиска на 135 582 строках ассемблерного кода для общего поискового хита (всего 21 769 хитов) привело к времени выполнения около 12 секунд для первого поиска и около 5 секунд для последующих поисков. Это не совсем желательное время ожидания для события GUI, поэтому мы должны были что-то предпринять. Выполнение поиска с помощью Unity Profiler показало, что 37,3% времени выполнения было потрачено на IndexOf(*), как показано ниже.

Разумная оптимизация должна устранить зависимость от этой функции, либо сделав собственную реализацию, либо полностью изменив алгоритм. Независимо от того, какой алгоритм будет использован, он будет включать в себя перебор всей строки. Таким образом, для поиска совпадений требуется некоторая собственная реализация. Учитывая это, представляется целесообразным начать оптимизацию с сохранения оригинального алгоритма, но создания собственной функции IndexOf.
3,34 секунды, потраченные на LongTextArea.GetFragNrFromBlockIdx(), связаны с получением неокрашенного кода сборки. Это используется для выполнения поиска. В настоящее время Burst Inspector сохраняет ассемблерный код дважды - один раз в формате для рендеринга, а второй раз в неформатированном виде.
Написание пользовательской функции также имеет приятный побочный эффект - сокращение количества вызовов, поскольку в настоящее время вызов выполняется для каждого поискового запроса плюс один.
Исходный код IndexOf(*) показывает множество проверок безопасности, необходимых для надежной общей реализации. Однако в нашем случае мы можем с уверенностью предположить, что большинство этих проверок верны. Чтобы попытаться выжать все до последней капли производительности, Вы захотите создать C-подобную функцию, чтобы избежать таких вещей, как проверка границ.
Вы можете написать функцию, следуя приведенному ниже псевдокоду, где IsKeyMatch(*) просто проверяет, совпадает ключ или нет.
List<int> Search(string assemblyCode, string key, int accIdx) {
var hits = new List<int>();
for (i = accIdx; i < assemblyCode.len - key.len; i++) {
if (IsKeyMatch(assemblyCode, key, i)) {
hits.add(i);
i += key.len-1;
}
}
return hits;
}
Однако, поскольку C# - управляемый язык, эта C-подобная функция требует, чтобы Вы прикрепили используемые управляемые объекты, чтобы сборщик мусора не перемещал адреса памяти. Вот шаблонный код:
unsafe {
fixed (char* source = assemblyCode) {
fixed (char* needle = key) {
CustomIndexOf(source, key)
}
}
}
Соединив эти вещи вместе, Вы сможете разделить исходный цикл while на один вызов программы поиска индексов и логику обработки результатов поиска:
matches = FindAllMathces(text, key)
foreach match {
...
Do logic for handling search hits
...
}
Каковы были достижения? Используя небольшой пример из предыдущей статьи, это изменение в коде дает 6,6-кратное ускорение при первом вызове и 13,2-кратное ускорение при последующих вызовах (измеряется как старый/новый). Меньшее ускорение при начальном поиске связано с затратами на загрузку неформатированной сборки, чтобы избежать поиска совпадений в цветовых строках.

Благодаря этим улучшениям поиск с большой нагрузкой, содержащий чуть менее 22 000 совпадений, теперь будет занимать около 1,8 секунды для первоначального поиска и около 0,4 секунды для последующих поисков. Это делает Burst Inspector более удобным для работы с большими сборками, так как теперь не нужно тратить время на приготовление чашки чая во время каждого поиска.
Вы можете воспользоваться этим улучшением производительности уже сейчас с пакетом Burst 1.8.7.
Хотите узнать больше о Burst? Общайтесь с нами на форуме Burst. Обязательно следите за новыми техническими блогами от других разработчиков Unity в рамках продолжающейсясерииTech from the Trenches.