Обработка строк и текста — распространенный источник проблем с производительностью в проектах Unity. В C# все строки являются неизменяемымиВы не можете изменить содержимое неизменяемого (доступного только для чтения) пакета. Это противоположность mutable. Большинство пакетов являются неизменяемыми, включая пакеты, загруженные из реестра пакетов или по URL-адресу Git.
См. в Словарь. Любые манипуляции со строкой приводят к выделению полной новой строки. Это относительно дорого, а повторяющиеся конкатенации строк могут привести к проблемам с производительностью при выполнении с большими строками, большими наборами данных или в узких циклах.
Кроме того, поскольку для конкатенации N строк требуется выделение N–1 промежуточных строк, последовательные конкатенации также могут быть основной причиной нехватки управляемой памяти.
В случаях, когда строки должны быть объединены в короткие циклы или во время каждого кадра, используйте StringBuilder для выполнения фактических операций объединения. Экземпляр StringBuilder также можно использовать повторно, чтобы свести к минимуму ненужное выделение памяти.
Microsoft поддерживает список рекомендаций по работе со строками в C#, который можно найти здесь на веб-сайте MSDN: msdn.microsoft.com.
Приведение региональных настроек и порядковые сравнения
Одной из основных проблем с производительностью, часто встречающихся в коде, связанном со строками, является непреднамеренное использование медленных строковых API по умолчанию. Эти API были созданы для бизнес-приложений и пытаются обрабатывать строки из множества различных культурных и лингвистических правил в отношении символов, встречающихся в тексте.
Например, следующий пример кода возвращает значение true при запуске в англо-американской локали, но возвращает значение false для многих европейских локалей.
Примечание. Начиная с Unity 5.3 и 5.4, среды выполнения скриптов Unity всегда работают в языковых стандартах английского языка США (en-US):
String.Equals("encyclopedia", “encyclopædia”);
Для большинства проектов Unity это совершенно не нужно. Примерно в десять раз быстрее использовать порядковый тип сравнения, который сравнивает строки способом, знакомым программистам на C и C++: просто сравнивая каждый последовательный байт строки без учета символа, представленного этим байтом.
Переключиться на сравнение строк по порядковому номеру так же просто, как указать StringComparison.Ordinal
в качестве последнего аргумента для String.Equals
:
myString.Equals(otherString, StringComparison.Ordinal);
Неэффективные встроенные строковые API
Помимо переключения на порядковые сравнения, известно, что некоторые API C# String
крайне неэффективны. Среди них String.Format
, String.StartsWith
и String.EndsWith
. . String.Format
сложно заменить, но неэффективные методы сравнения строк легко оптимизируются.
Хотя Microsoft рекомендует передавать StringComparison.Ordinal
в любое сравнение строк, которое не нужно корректировать для локализации, тесты Unity показывают, что влияние этого относительно минимально. по сравнению с пользовательской реализацией.
Метод | Время (мс) для 100 000 коротких строк |
---|---|
String.StartsWith , default culture |
137 |
String.EndsWith , default culture |
542 |
String.StartsWith , ordinal |
115 |
String.EndsWith , ordinal |
34 |
Custom StartsWith replacement |
4.5 |
Custom EndsWith replacement |
4.5 |
И String.StartsWith
, и String.EndsWith
можно заменить простыми версиями, написанными вручную, как в прилагаемом примере. ниже.
public static bool CustomEndsWith(this string a, string b)
{
int ap = a.Length - 1;
int bp = b.Length - 1;
while (ap >= 0 && bp >= 0 && a [ap] == b [bp])
{
ap--;
bp--;
}
return (bp < 0);
}
public static bool CustomStartsWith(this string a, string b)
{
int aLen = a.Length;
int bLen = b.Length;
int ap = 0; int bp = 0;
while (ap < aLen && bp < bLen && a [ap] == b [bp])
{
ap++;
bp++;
}
return (bp == bLen);
}
Регулярные выражения
Несмотря на то, что регулярные выражения представляют собой мощный способ сопоставления строк и управления ими, они могут быть чрезвычайно требовательны к производительности. Кроме того, благодаря реализации регулярных выражений в библиотеке C# даже простые логические запросы IsMatch
выделяют большие временные структуры данных «под капотом». Этот временный управляемый отток памяти следует считать неприемлемым, кроме как во время инициализации.
Если необходимы регулярные выражения, настоятельно рекомендуется не использовать статические методы Regex.Match
или Regex.Replace
, которые принимают регулярное выражение в качестве строкового параметра. Эти методы компилируют регулярное выражение на лету и не кэшируют сгенерированный объект.
Этот пример кода является безобидным однострочником.
Regex.Match(myString, "foo");
Однако каждый раз при выполнении создается 5 килобайт мусора. Простой рефакторинг может устранить большую часть этого мусора:
var myRegExp = new Regex("foo");
myRegExp.Match(myString);
В этом примере каждый вызов myRegExp.Match
«только» приводит к 320 байтам мусора. Хотя это по-прежнему дорого для простой операции сопоставления, это значительное улучшение по сравнению с предыдущим примером.
Поэтому, если регулярные выражения являются инвариантными строковыми литералами, гораздо эффективнее предварительно скомпилировать их, передав их в качестве первого параметра конструктору объекта Regex. Затем эти предварительно скомпилированные регулярные выражения следует использовать повторно.
Синтаксический анализ XML, JSON и других длинных текстов
Синтаксический анализ текста часто является одной из самых сложных операций, выполняемых во время загрузки. Иногда время, затрачиваемое на синтаксический анализ текста, может перевешивать время, затрачиваемое на загрузку и создание объектов.
Причины этого зависят от конкретного используемого парсера. Встроенный в C# анализатор XML чрезвычайно гибок, но в результате его нельзя оптимизировать для конкретных макетов данных.
Многие сторонние синтаксические анализаторы основаны на отражении. Хотя отражение является отличным выбором во время разработки (поскольку оно позволяет синтаксическому анализатору быстро адаптироваться к изменяющимся макетам данных), оно, как известно, работает медленно.
Unity представила частичное решение со встроенным API JSONUtility, который предоставляет интерфейс к системе сериализации Unity, которая считывает/отправляет JSON. В большинстве тестов он работает быстрее, чем парсеры JSON на чистом C#, но имеет те же ограничения, что и другие интерфейсы системы сериализации Unity: он не может сериализовать многие сложные типы данных, такие как словари, без дополнительного кода.
Примечание. См. интерфейс ISerializationCallbackReceiver, чтобы узнать, как добавить дополнительную обработку, необходимую для преобразования в сложные типы данных или из них. в процессе сериализации Unity.
При возникновении проблем с производительностью, возникающих при синтаксическом анализе текстовых данных, рассмотрите три альтернативных решения.
Вариант 1. Анализировать во время сборки
Лучший способ избежать затрат на синтаксический анализ текста — полностью отказаться от синтаксического анализа текста во время выполнения. В общем, это означает «запекание» текстовых данных в двоичном формате на каком-то этапе сборки.
Большинство разработчиков, которые выбирают этот путь, перемещают свои данные в некую иерархию классов, производных от ScriptableObject, а затем распространяют данные через AssetBundles. Отличное обсуждение использования ScriptableObjects см. в выступлении Ричарда Файна на Unite 2016 на YouTube. р>
Эта стратегия обеспечивает максимально возможную производительность, но подходит только для данных, которые не нужно генерировать динамически. он лучше всего подходит для параметров игрового дизайна и другого контента.
Вариант 2. Раздельная и отложенная загрузка
Вторая возможность – разбить данные, которые должны быть проанализированы, на более мелкие фрагменты. После разделения стоимость синтаксического анализа данных может быть распределена между несколькими кадрами. В идеальном случае определите определенные части данных, которые необходимы для предоставления пользователю желаемого опыта, и загрузите только эти части.
В простом примере: если бы проект был игрой-платформером, не было бы необходимости сериализовать данные для всех уровней вместе в один гигантский блоб. Если бы данные были разделены на отдельные активы для каждого уровня и, возможно, сегментированы уровни на регионы, данные можно было бы анализировать по мере приближения игрока к ним.
Хотя это звучит просто, на практике это требует значительных инвестиций в программный код и может потребовать реорганизации структур данных.
Вариант 3. Темы
Для данных, которые полностью преобразуются в простые объекты C# и не требуют взаимодействия с API Unity, можно перенести операции анализа в рабочие потоки.
Эта опция может быть чрезвычайно эффективной на платформах со значительным количеством ядер. Однако это требует тщательного программирования, чтобы избежать взаимоблокировок и условий гонки.
Примечание. iOSмобильная операционная система Apple. Подробнее
См. в Словарь устройства имеют не более 2 ядер. У большинства Android-устройств их от 2 до 4. Этот метод более интересен при сборке для автономных и консольных целевых сборок.
Проекты, в которых реализована многопоточность, используют встроенный C# Thread и ThreadPool (см. msdn.microsoft.com) для управления своими рабочими потоками вместе со стандартными классами синхронизации C#.