Русский
Русский
English
Статистика
Реклама

Оптимизации

Ещё больше строковых оптимизаций

30.11.2020 18:20:43 | Автор: admin

В продолжение своей предыдущей статьи о строках (напоминаю, это была текстовая версия доклада на конференции JPoint-2020) решил дописать ещё одну заметку со строковыми оптимизациями, обнаруженными уже после вёрстки презентации (первые две есть на видео в самом конце, показывал их прямо из "Идеи").

Снова StringBuilder.append(char)

На сцене снова "Спринг", а именно o.s.u.StringUtils.deleteAny(String, String):

// org.springframework.util.StringUtilspublic static String deleteAny(String inString, String charsToDelete) {  if (!hasLength(inString) || !hasLength(charsToDelete)) {    return inString;  }  StringBuilder sb = new StringBuilder(inString.length());  for (int i = 0; i < inString.length(); i++) {    char c = inString.charAt(i);    if (charsToDelete.indexOf(c) == -1) {      sb.append(c);    }  }  return sb.toString();}

В разделе "Склейка: если всё-таки нужно" рассматривая StringBuilder.append(char) я отметил невозможность оптимизации компилятором проверок внутри этого метода даже в счётном цикле с заранее известным количеством проходов. Выходом станет использование массива.

Этот же подход хорошо ложится на случаи, в которых длина преобразованной строки может быть меньше или равна исходной. Действительно, какой бы аргумент не передавался в deleteAny, длина возвращаемой строки никогда не превысит длину исходной. Следовательно, можно незначительно усложнив код избавиться от SB:

public static String deleteAny(String inString, String charsToDelete) {  if (!hasLength(inString) || !hasLength(charsToDelete)) {    return inString;  }    int lastCharIndex = 0;  char[] result = new char[inString.length()];  for (int i = 0; i < inString.length(); i++) {    char c = inString.charAt(i);    if (charsToDelete.indexOf(c) == -1) {      result[lastCharIndex++] = c;    }  }  return new String(result, 0, lastCharIndex);}

Переменная lastCharIndex устраняет возможные "пустоты" в массиве при пропуске одного и более знаков.

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

Benchmark                       Mode     Score     Error   Unitsoriginal                        avgt    90.203    4.317   ns/oppatched                         avgt    25.391    1.118   ns/oporiginal:gc.alloc.rate.norm    avgt   104.000    0.001    B/oppatched:gc.alloc.rate.norm     avgt   104.000    0.001    B/op

Но и это ещё не всё.

Уже после моего коммита наблюдательный пользователь Johnny Lim сообразил, что если после завершения перебора значение переменной lastCharIndex равно длине исходной строки, то ни одной замены не сделано, а значит новую строку создавать не нужно. Итого:

public static String deleteAny(String inString, String charsToDelete) {  if (!hasLength(inString) || !hasLength(charsToDelete)) {    return inString;  }    int lastCharIndex = 0;  char[] result = new char[inString.length()];  for (int i = 0; i < inString.length(); i++) {    char c = inString.charAt(i);    if (charsToDelete.indexOf(c) == -1) {      result[lastCharIndex++] = c;    }  }  if (lastCharIndex == inString.length()) {   // С - сообразительность    return inString;  }  return new String(result, 0, lastCharIndex);}

Похожий подход использован тут.

Коварный StringBuilder.append(Object)

Следующий пример намного менее очевиден:

// org.springframework.http.ContentDispositionprivate static String escapeQuotationsInFilename(String filename) {  if (filename.indexOf('"') == -1 && filename.indexOf('\\') == -1) {    return filename;  }  boolean escaped = false;  StringBuilder sb = new StringBuilder();  for (char c : filename.toCharArray()) {    sb.append((c == '"' && !escaped) ? "\\\"" : c);    // <----    escaped = (!escaped && c == '\\');  }  // Remove backslash at the end..  if (escaped) {    sb.deleteCharAt(sb.length() - 1);  }  return sb.toString();}

Больное место находится в строке 10, а заключение звучит так: поскольку тернарный оператор возвращает либо String, либо char, то аргументом метода StringBuilder.append() фактически является Object. И всё бы ничего, да преобразование знака в объект происходит с помощью String.valueOf() и мы на ровном месте получаем множество мелкого мусора по схеме:

Character.valueOf(c)   -> StringBuilder.append(Object)     -> String.valueOf()      -> Character.toString()

Отдельно доставляет реализация Character.toString:

Осторожно, не ушибите лицо ладонью
public final class Character {  private final char value;  public String toString() {    char buf[] = {value};    return String.valueOf(buf);  }}

Зачем было оборачивать знак в массив? Тайна сия велика... Как бы то ни было, это уже исправлено.

Таким образом, достаточно избавиться от тернарного оператора:

for (int i = 0; i < filename.length() ; i++) {  char c = filename.charAt(i);  if (!escaped && c == '"') {    sb.append("\\\"");    // append(String)  }  else {    sb.append(c);         // append(char)  }  escaped = (!escaped && c == '\\');}

И вновь очень простое изменение приносит впечатляющий прирост (бенчмарк по ссылке):

JDK 8Benchmark                             latin   len   Score          UnitsappendCovariant                        true    10   180.2  10.3   ns/opappendExact                            true    10    68.5   1.4   ns/opappendCovariant                       false    10   177.7   4.4   ns/opappendExact                           false    10    67.7   1.3   ns/opappendCovariant:gc.alloc.rate.norm    true    10   688.0   0.0    B/opappendExact:gc.alloc.rate.norm        true    10   112.0   0.0    B/opappendCovariant:gc.alloc.rate.norm   false    10   816.0   0.0    B/opappendExact:gc.alloc.rate.norm       false    10   112.0   0.0    B/opJDK 14Benchmark                             latin   len    Score         UnitsappendCovariant                        true    10    228.8  18.6  ns/opappendExact                            true    10     57.9   2.6  ns/opappendCovariant                       false    10    292.8  12.4  ns/opappendExact                           false    10     90.2   2.2  ns/opappendCovariant:gc.alloc.rate.norm    true    10    688.0   0.0   B/opappendExact:gc.alloc.rate.norm        true    10    112.0   0.0   B/opappendCovariant:gc.alloc.rate.norm   false    10   1096.0   0.0   B/opappendExact:gc.alloc.rate.norm       false    10    200.0   0.0   B/op

Обратите внимание, что исполнение не смогло выбросить мусорные объекты, хотя их область видимости крайне ограничена. Закономерно возникает вопрос: могёт ли Грааль?

Ответ

Не знаю, не проверял :)
Оставляю этот вопрос энтузиастам в качестве домашнего задания :)

Коварный String.substring

Давно известно, что метод String.substring всегда возвращает новую строку, и тем не менее в задачах на "выкусывание" он всё ещё пользуется незаслуженной популярностью:

// org.springframework.web.util.UrlPathHelper/** * Sanitize the given path replacing "//" with "/" */private String getSanitizedPath(String path) {  String sanitized = path;  while (true) {    int idx = sanitized.indexOf("//");    if (idx < 0) {      break;    }    else {      sanitized = sanitized.substring(0, idx) + sanitized.substring(idx + 1);    }  }  return sanitized;}

Здесь даже если исполнение каким-то чудом сможет распознать шаблон склеивания строк и подставить StringBuilder этот метод всё равно останется чертовски неэффективным.

В такого рода задачах очень важно посмотреть на алгоритм с высокого уровня и описать простыми словами выполняемую работу. Здесь получается так: "заменить все двойные вхождения косой черты на единичные". Из строки знаки мы удалять не можем, но можем удалять из StringBuilder-а, а значит код выше можно превратить в этот:

private static String getSanitizedPath(String path) {  int index = path.indexOf("//");  if (index >= 0) {    StringBuilder sanitized = new StringBuilder(path);    while (index != -1) {      sanitized.deleteCharAt(index);      index = sanitized.indexOf("//", index); //не начинай сначала ;)    }    return sanitized.toString();  }  return path;}

В некоторых случаях использование StringBuilder.deleteCharAt(int) позволяет существенно облегчить понимание кода:

// org.springframework.web.util.UrlPathHelperprivate String removeSemicolonContentInternal(String requestUri) {  int semicolonIndex = requestUri.indexOf(';');  while (semicolonIndex != -1) {    int slashIndex = requestUri.indexOf('/', semicolonIndex);    String start = requestUri.substring(0, semicolonIndex);    requestUri = (slashIndex != -1) ? start + requestUri.substring(slashIndex) : start;    semicolonIndex = requestUri.indexOf(';', semicolonIndex);  }  return requestUri;}

Логика здесь довольно запутанная, но на высоком уровне метод удаляет содержимое, выделенное ; внутри ссылки, превращая строку вроде /foo;f=F;o=O;o=O/bar;b=B;a=A;r=R в /foo/bar;b=B;a=A;r=R.

Можно избавиться от взятия подстроки и склейки переписав метод вот так:

private static String removeSemicolonContentInternal(String requestUri) {  int semicolonIndex = requestUri.indexOf(';');  if (semicolonIndex == -1) {    return requestUri;  }  StringBuilder sb = new StringBuilder(requestUri);  while (semicolonIndex != -1) {    int slashIdx = sb.indexOf("/", semicolonIndex + 1);    if (slashIdx == -1) {      return sb.substring(0, semicolonIndex);    }    sb.delete(semicolonIndex, slashIdx);    semicolonIndex = sb.indexOf(";", semicolonIndex);  }  return sb.toString();}

На первый взгляд, кода стало больше: 12 строк превратились в 16. С другой стороны, он стал выразительнее и проще для понимания, что идёт приятной добавкой к улучшенной производительности.

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

Мопед не мой

В этом разделе описано улучшение, автором которого я не являюсь, но которое представляет интерес.

// org.springframework.util.StringUtilspublic static String trimLeadingWhitespace(String str) {  if (!hasLength(str)) {    return str;  }  StringBuilder sb = new StringBuilder(str);  while (sb.length() > 0 && Character.isWhitespace(sb.charAt(0))) {    sb.deleteCharAt(0);  }  return sb.toString();}

Чтобы улучшить этот код нужно вновь понять его высокоуровневый смысл (к счастью он полностью соответствует имени метода). Код удаляет все пробелы в начале строки, а значит вместо познакового удаления можно взять подстроку начиная от первого знака-непробела:

public static String trimLeadingWhitespace(String str) {  if (!hasLength(str)) {    return str;  }  int idx = 0;  while (idx < str.length() && Character.isWhitespace(str.charAt(idx))) {    idx++;  }  return str.substring(idx);}

Этот код значительно быстрее первоначальной версии, а также потребляет меньше памяти, ведь в худшем случае создаётся только один объект (в лучшем при idx = 0 метод str.substring()вернёт строку, на которой он был вызван).

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

Мелочь

Если в коде есть что-то вроде

char[] chars;//...return new String(chars, 0, chars.length);

то это всегда можно переписать в виде

char[] chars;//...return new String(chars);

Производительность это сильно не улучшит, однако, перефразируя рекламу моющего средства из 90-х, "если не видно разницы, то зачем писать больше?" :)

Заключение

На этом всё, надеюсь примеры были вам интересны и полезны. Описывайте свои улучшения в комментариях, самые интересные добавлю в статью. До новых встреч!

Подробнее..

Проблемы с производительностью в игре XCOM 2

15.04.2021 08:23:06 | Автор: admin

Привет! Меня зовут Александр, я руководитель программистов компьютерной графики в Gaijin в проектах CRSED и Enlisted. Иногда, в свободное время, я исследую как устроена графика в других играх и нахожу там что-то интересное.

Недавно я решил разобраться, почему XCOM 2 тормозит на моём ноутбуке. В ходе изучения рендера этой игры я нашёл ряд мест, которые можно было бы без проблем ускорить. Результаты моего небольшого исследования вылились в видео: https://www.youtube.com/watch?v=CuPPc2Z8lTk

Ниже представлена расшифровка этого видео.


Добрый день!

Вероятно, вы играли в игру XCOM 2 или хотя бы слышали о ней. Она вышла в 2016 году. Сделана на движке Unreal Engine 3.5. Если оценивать XCOM как игру в целом, мне она понравилась. Увлекательный геймплей, приятная картинка, интересная история.

Единственная проблема, с которой я столкнулся, это низкий FPS, в особенности на кадрах с выстрелами крупным планом. На базе и в тактическом виде эта проблема менее заметна. Средний FPS у меня был в районе 25-30. И мне стало интересно, выжимает ли игра все доступные мощности из моей ноутбучной GTX 1050 или можно сделать лучше. Сейчас я покажу вам 6 оптимизаций, которые могли помочь разработчикам улучшить производительность данной игры.

Захват кадров

Для анализа графики я использовал RenderDoc версии 1.12. Он без проблем захватил несколько кадров, которые я потом просмотрел. Я взял один кадр из меню, кадр базы, кадр на тактической карте и кадр с выстрелом.

Во всех них наблюдаются общие проблемы с производительностью. Проходы, которые вы здесь видите (скриншот ниже), это последовательные вызовы отрисовки, для которых выставлены одни и те же рендертаргеты, т.е. текстуры, в которые рисуется результат.

Жирный G-buffer

Первая оптимизация связана с уменьшением размера G-buffer'а. Самый долгий проход это заполнение G-buffer'а (>16 мс). Это видно как на таймингах различных проходов, так и на общем таймлайне.

Всего в G-buffer входит 5 текстур в формате RGBA16F, то есть текстуры имеют 4 16-битных канала и содержат вещественные числа.

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

Итак, G-buffer содержит следующие текстуры:

  1. Цвета эмиссивных (т.е. светящихся) материалов (причём альфа-канал этой текстуры пустой).

  2. Альбедо или просто цвет без учета освещения (альфа-канал содержит Ambient Occlusion).

  3. Нормали (в альфа-канале хранится номер одного из 4 материалов)

  4. Параметры материалов (цвет металла + roughness).

  5. Дополнительные нормали для анизотропных материалов (транслюсентность в альфа-канале это параметр, показывающий насколько поверхность пропускает свет сквозь себя)

У текстуры эмиссива можно было бы удалить четвертый канал. И тем самым вместо 16 Мб потребуется 12 Мб.

Текстуру альбедо вполне можно было бы хранить как 4 8-битных канала с нормализованными вещественными числами (то есть числами от 0 до 1). Это уменьшило бы эту текстуру в 2 раза. До 8 Мб.

Нормали хранятся в сыром виде. Можно упаковывать их при записи, тем самым снижая количество данных, и распаковывать при чтении [Подробнее можно прочитать тут]. Это, конечно, требует больше времени на выполнение кода, но существенно снижает количество требуемых данных.

Материал принимает всего 4 различных значения, значит, отлично пакуется в 2 бита. Предположим, что эти два бита мы положили к параметрам материалов. Тогда для нормалей остаются 2 канала по 16 бит каждый. Всего 8 Мб для моего разрешения экрана.

Параметры материалов оставим без изменения, за исключением кодирования номера материала в эту же текстуру.

Последняя текстура параметры для транслюсентных материалов. Первые 3 компоненты это единичные векторы, значит, их тоже можно закодировать в 2 вещественных числа. Остаётся 3 канала. Причём транслюсентные материалы не эмиссивные. По крайней мере, в захваченных кадрах я такого не видел. Значит, можно объединить эту текстуру с текстурой эмиссива, и на неё мы теперь тратим 0 Мб.

Итого, нам нужно 12 Мб для эмиссива и транслюсентности, 8 Мб для диффуза, 8 Мб для нормалей и 16 для параметров материалов. Всего 44 Мб. Почти в два раза меньше памяти. Думаю, это сильно бы ускорило проход для заполнения G-buffer.

Отсутствие объектов в предварительном проходе

Другая оптимизация, которая могла бы уменьшить количество записываемых данных в G-buffer, это более агрессивное использование предварительного прохода (prepass). Prepass это предварительная отрисовка сцены в буфер глубины. Выполняется она с целью уменьшить количество перезаписей пикселей G-buffer'а за счёт отбрасывания пикселей, не прошедших тест глубины. Текущий предварительный проход оптимизирует отрисовку, но можно добиться и лучших результатов.

При записи G-buffer'а некоторые пиксели перерисовываются до 24 раз.

Судя по вызовам драйвера, между prepassом и G-buffer пассом нет никаких копирований текстуры глубины или чтений этой текстуры на CPU. Значит, теоретически, всю геометрию, которая рисуется в G-buffer, можно было нарисовать в prepassе. Таким образом, можно было бы сделать ещё быстрее. И учитывая, что это самый долгий проход во всём кадре, оптимизация не была бы лишней.

Не используется инстанцирование

Оставим пиксельные оптимизации и обратимся к геометрии. Как вы могли заметить (обратите внимание на вызовы DrawIndexed на предыдущем скриншоте), объекты рисуются строго по одному. Это связано с тем, что для отрисовки используется вызов DrawIndexed вместо DrawIndexedInstanced, который позволяет рисовать несколько одинаковых объектов за раз.

А одинаковых объектов тут много. Не вдаваясь в подробности выполнения отдельных вызовов отрисовки и того, в каком порядке и как видеокарта их выполняет, хочу отметить, что при использовании инстанцирования потребовалось бы намного меньше вызовов функций DirectX, а значит меньше команд отправлялось бы на видеокарту. Уже это могло бы дать прирост FPS.

Level of Details

И последняя оптимизация связанная с рисованием сцены это система level of details. Нет смысла рисовать детализированную геометрию, если она вдалеке и занимает пару десятков пикселей.

Во-первых, субпиксельные треугольники замедляют рендер. Подробнее можете прочитать в данной статье. Во-вторых, в этом нет практического смысла. Например, из почти тысячи треугольников этого объекта мы увидим разве что пару десятков.

Использование менее детальной геометрии могло бы заметно уменьшить количество рисуемых треугольников. Естественно, это ускорило бы рендер.

Полноэкранный SSAO (Screen Space Ambient Occlusion)

Второй по длительности проход после заполнения G-buffer'а это подготовка текстуры SSAO. Она занимает от 8 до 10 мс. И проблема этого прохода в том, что он полноэкранный.

Как я рассказывал на стриме по GTAO, подобные эффекты лучше делать в половинном разрешении экрана. У профессионалов из Activision Blizzard получилось уместить отрисовку AO в половину миллисекунды. Они замеряли на PlayStation 4, а я на ноутбуке и сравнивать время таким образом не до конца корректно. Тем не менее отмечу, что у моей видеокарты в 2.5 раза меньше GFLOPS, а вычисление AO в игре медленнее в 20 раз чем в статье от Blizzard. В общем, думаю можно сделать вывод, что полноэкранный проход для AO может быть значительно ускорен.

Depth of Field

И последнее очевидное узкое место это depth of field. В XCOM реализован очень интересный подход к этому эффекту. Рисуются 3 миллиона треугольников. Каждый из них соответствует пикселю текстуры в половинном разрешении экрана.

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

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

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

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru