Для некоторых оптимизаций требуются сложные структуры данных и тысячи строк кода. В других же случаях серьёзный прирост производительности даёт минимальное изменение: иногда нужно лишь поставить ноль. Это похоже на старую байку о котельщике, который знает правильное место для удара молотком, а потом выставляет клиенту счёт: $0,50 за удар по клапану и $999,50 за знание, куда бить.
Я лично встречал несколько ошибок производительности, которые исправлялись вводом одного нуля, и в этой статье хочу поделиться двумя историями.
Важность измерения
Во времена оригинальной Xbox я помог оптимизировать множество игр. В одной из них профилировщик указал на функцию матричного преобразования, которая потребляла 7% времени CPU — самый большой скачок на графике. Поэтому я прилежно приступил к работе по оптимизации этой функции.
Видно, что я не первый пытался это сделать. Функцию уже переписали на ассемблере. Я нашёл несколько потенциальных улучшений в языке ассемблера и попытался измерить их эффект. Это важный шаг, иначе легко заняться «оптимизацией», которая ничего не изменит или даже ухудшит ситуацию.
Однако измерение оказалось затруднено. Я запускал игру, немного играл с параллельным профилированием, а затем изучал профиль: стал ли код быстрее. Казалось, что есть какое-то небольшое улучшение, но нельзя было сказать наверняка.
Так что я применил научный метод. Написал коллекцию тестов для управления старой и новой версиями кода, чтобы точно измерить различия в производительности. Это не заняло много времени: как и ожидалось, новый код оказался примерно на 10% быстрее старого.
Но выясмнилось, что 10% ускорения — это ерунда.
Гораздо интереснее, что внутри теста код выполнялся примерно в 10 раз быстрее, чем в игре. Вот это было захватывающее открытие.
После проверки результатов я некоторое время смотрел в пустоту, но потом меня осенило.
Роль кэширования
Чтобы дать разработчикам игр полный контроль и максимальную производительность, игровые приставки позволяют выделять память с различными атрибутами. В частности, оригинальный Xbox позволяет выделять некэшируемую память. Этот тип памяти (фактически, тип тега в таблицах страниц) полезен при записи данных для GPU. Поскольку память не кэшируется, запись почти сразу пойдёт в RAM без задержек и загрязнения кэша при «нормальном» мэппинге.
Таким образом, некэшируемая память — важная оптимизация, но её следует использовать осторожно. В частности, крайне важно, чтобы игры никогда не пытались читать из некэшируемой памяти, иначе их производительность серьёзно снизится. Даже относительно медленному CPU на 733 МГц в оригинальном Xbox нужны свои кэши, чтобы обеспечить достаточную производительность при чтении данных.
Теперь становится понятно, что происходит. Судя по всему для этой функции данные выделяются в некэшируемой памяти, отсюда низкая производительность. Небольшая проверка подтвердила эту гипотезу, так что пришло время исправить проблему. Я нашёл строку, где выделяется память, дважды щёлкнул по значению флага, и указал ноль.
Вместо примерно 7% процессорного времени функция стала потреблять около 0,7% и больше не представляла проблемы.
По итогам недели мой отчёт выглядел примерно так: «39,999 часа исследований, 0,001 часа программирования — огромный успех!»
Разработчикам обычно не нужно беспокоиться о случайном выделении некэшируемой памяти: в большинстве операционных систем эта опция не доступна в пользовательском пространстве стандартными методами. Но если вам интересно, насколько некэшируемая память способна замедлить работу программы, попробуйте флаги PAGE_NOCACHE или PAGE_WRITECOMBINE в VirtualAlloc.
0 ГиБ лучше, чем 4 ГиБ
Хочу рассказать вам ещё одну историю. Она о баге, который нашёл я, а исправил кто-то другой. Пару лет назад я заметил, что дисковый кэш на моём ноутбуке слишком часто прочищается. Я отследил, что это происходит при достижении рубежа 4 ГиБ, и в итоге оказалось, что драйвер моего нового HDD для бэкапов устанавливает SectorSize в 0xFFFFFFFF (или ?1) при указании на неизвестный размер сектора. Ядро Windows интерпретирует это значение как 4 ГиБ и выделяет соответствующий блок памяти, что и стало причиной проблемы.
У меня нет контактов в Western Digital, но можно с уверенностью предположить, что они исправили эту ошибку, заменив константу 0xFFFFFFFF (или ?1) на ноль. Один введённый символ — и решена серьёзная проблема производительности.
(Подробнее об этом исследовании читайте в статье «Замедление Windows: изучение и идентификация»)
Наблюдения
- В обоих случаях проблема связана с кэшированием
- Решающим стало использование профилировщика для точного определения проблемы
- Если патч не проверен измерениями, то он не обязательно поможет
- Я мог бы написать о многих других таких случаях, но они либо слишком секретны, либо слишком скучны
- Правильное решение не обязательно должно быть сложным. Иногда огромное улучшение даёт небольшое изменение. Нужно только знать, в каком месте
Мне случалось оптимизировать код, расскоментив #define и путём других тривиальных изменений. Расскажите в комментариях, если у вас есть такие истории.
Комментарии (38)
walti
23.12.2018 15:18Почему Microsoft просто не поставили более мощный процессор в приставку?
Ведь разработка нового процессора стоит меньше, чем зарплата программиста.Sultansoy
23.12.2018 16:00+5А ведь именно из-за того мышления мы сейчас получаем лагающие игры даже на новых i9 в связке с 2080.
Код должен быть оптимизирован всегда. А херовый код даже лучший процессор не вытянет.timdorohin
23.12.2018 17:48+8Я искренне подозреваю что сказанное про процессор было сарказмом, просто его не поняли. Ведь разработка процессора намного дороже чем найти баг в программе.
JediPhilosopher
23.12.2018 22:07+3У приставок своя атмосфера. В отличие от настольных ПК они не подлежат апгрейду и имеют при этом гораздо более долгий срок жизни. Поэтому в любом случае даже супер-пупер железо на момент выхода приставки (а точнее на момент начала ее разработки) уже через несколько лет успеет устареть и разработчикам придется выкручиваться.
Я помню очень удивлялся, как на PS3 с ее жалкими 512 (кажется) Мб памяти летали игры, которые лагали на ПК с двумя гигами. Сколько ухищрений, оптимизаций и упрощений запихивали разработчики туда.
questor
23.12.2018 15:54Кеширование — вообще непростая штука и я чисто видел случаи, где пытаясь сделать лучше только убивали производительность именно тем, что «а давайте мы поставим кеширование».
Ошибки такого рода больше свойственны начинающим, кто плохо понимает механику работы и кто не умеет пользоваться инструментами замера производительности.blind_oracle
26.12.2018 13:43There are only two hard things in Computer Science: cache invalidation and naming things.
— Phil Karlton
simik2
23.12.2018 18:33Сильно увеличил размер индекса в чей-то реализации хэш-таблицы. На малом кол-ве элементов хэш работал нормально, а на миллионах вставал колом.
xytop
23.12.2018 20:58Исправил регулярное выражение, тем самым увеличив скорость комплиляции шаблонов Laravel в 40 раз :)
411
24.12.2018 06:33Тоже была подобная проблема с регулярками, только для проверки на то, что строка является ссылкой(целиком). Правда решил не исправлением большой регулярки, а проверкой каждой части ссылки своей регуляркой.
faiwer
24.12.2018 07:27Лет 5 назад столкнулся с интересной проблемой catastrophic backtracing в regular expressions (статья не моя, просто пример). Это когда визуально вроде всё нормально, и оно даже работает. Но с увеличением длины строки производительность гасится весьма нелинейно. Патч в 1-2 символа. С тех пор стал писать их куда внимательнее :)
wickedweasel
24.12.2018 13:38Ускорил файловый кэш (на секундочку, сам кэш призван ускорять) в бородатом симфони 1.2 в 3-10 раз
dimonoid
23.12.2018 21:32+1Нужно просто разрабатывать на процессоре (пусть даже i9), но на пониженной частоте строго 2-3ghz, а запускать потребителю — на нормальных 4-5ghz. Вот и весь секрет скорости.
vesper-bot
24.12.2018 11:29-1Не разрабатывать, а отлаживать (тестировать). И желательно частоту вообще где-то в 1ГГц загнать, но заставить ПО использовать все доступные ядра, чтобы ещё и многопоточные ошибки проявлялись почаще.
cynovg
23.12.2018 22:47Кто-то закомментировал кеширование при формировании списка товаров для отладки и забыл вернуть. Исправил это спустя год-полтора :-)
Goodkat
24.12.2018 00:55Добавил в таблицу индекс, отчёт стал готовиться за 40 секунд вместо 40 минут.
0xf0x
24.12.2018 01:16У нас один добрый человек выводил hp-bar'ы над юнитами, используя stencil bufffer, очищая его каждый раз перед отрисовкой нового bar'a. Простая замена на scirrors дала более чем двукратный прирост fps:)
Azrael33
24.12.2018 10:32>> Добавил в таблицу индекс…
Удалил сложный индекс в таблице-снизил число блокировок и увеличил производительность БД
nmrulin
24.12.2018 11:55Одни из самых противных ошибок это те которые не приводят напрямую к вылету, но зато грузят процессор.
achekalin
24.12.2018 12:12«Байка о котельщике» — понятно, что перевод, но все же её в России про Капицу рассказывают, а не про котельщика.
ads83
24.12.2018 12:59А я читал версию, что на атомной станции были проблемы. Мелкие, но неприятные, и это атомная станция. И как старенький профессор пометил карандашом проблемную трубу, оказался тысячу раз прав, и в конце рассказа объяснял, что взял деньги не за время, потраченное на решение проблемы, а за знание, что и где смотреть
P.S. Читал давно, помню только общий сюжет и найти сходу не смог :(wickedweasel
24.12.2018 13:36Ага, фрилансеры чинят атомную станцию.
AlexanderG
24.12.2018 17:57А про забытые в первом контуре ключи и даже фуфайки, к сожалению, не шутки (с мертвого форума):
— А откуда вообще мусор то? Ну поменяли задвижку ну и что?
(случайно забытые гаечные ключи, болты-гайки, ставшие «лишними» я в расчет не беру).
— Так дело в том, что чаще всего именно ключи, гайки и т.д. оттуда и выносятся. Это что, бывало вылавливали фуфайки на фильтрах РГК — почти вход в активную зону.
— Не, это, конечно, мусор, но я думал что такой мусор (болты, ключи) исключен в принципе.Я понимаю что моя наивность не знает границ
mdaemon
24.12.2018 14:12Частый пробег по одному полю из массива структур тормозил. Вынесение значения этого поля из структур в отдельный массив решило вопрос. (оказалось за годы структуры разрослись и начались промахи кэша...)
Shpiler
Буквально вчера был эпизод увеличения производительности в ~72 раза одной кнопкой. Это было delete по вызову логирования, который по некоторым историческим причинам попал внутрь одной функции, вызываемой при численном интегрировании сложной матмодели. Профилировщик, правда, для этого не понадобился, просто нужно помнить что логи — штука медленная, и если пишешь их для отладки, то после себя — удалять, а не комментировать на случай «а вдруг оно опять понадобится». Не понадобится. Зато кто-нибудь другой может залезть в код и сказать «о! логи закоменчены!» и вернуть всё взад.
third112
А можно добавить еще один коммент: " логи — штука медленная!"? :)