
Для некоторых оптимизаций требуются сложные структуры данных и тысячи строк кода. В других же случаях серьёзный прирост производительности даёт минимальное изменение: иногда нужно лишь поставить ноль. Это похоже на старую байку о котельщике, который знает правильное место для удара молотком, а потом выставляет клиенту счёт: $0,50 за удар по клапану и $999,50 за знание, куда бить.
Я лично встречал несколько ошибок производительности, которые исправлялись вводом одного нуля, и в этой статье хочу поделиться двумя историями.
Важность измерения


Однако измерение оказалось затруднено. Я запускал игру, немного играл с параллельным профилированием, а затем изучал профиль: стал ли код быстрее. Казалось, что есть какое-то небольшое улучшение, но нельзя было сказать наверняка.
Так что я применил научный метод. Написал коллекцию тестов для управления старой и новой версиями кода, чтобы точно измерить различия в производительности.

Но выясмнилось, что 10% ускорения — это ерунда.
Гораздо интереснее, что внутри теста код выполнялся примерно в 10 раз быстрее, чем в игре. Вот это было захватывающее открытие.
После проверки результатов я некоторое время смотрел в пустоту, но потом меня осенило.
Роль кэширования
Чтобы дать разработчикам игр полный контроль и максимальную производительность, игровые приставки позволяют выделять память с различными атрибутами. В частности, оригинальный Xbox позволяет выделять некэшируемую память. Этот тип памяти (фактически, тип тега в таблицах страниц) полезен при записи данных для GPU. Поскольку память не кэшируется, запись почти сразу пойдёт в RAM без задержек и загрязнения кэша при «нормальном» мэппинге.
Таким образом, некэшируемая память — важная оптимизация, но её следует использовать осторожно. В частности, крайне важно, чтобы игры никогда не пытались читать из некэшируемой памяти, иначе их производительность серьёзно снизится. Даже относительно медленному CPU на 733 МГц в оригинальном Xbox нужны свои кэши, чтобы обеспечить достаточную производительность при чтении данных.
Теперь становится понятно, что происходит. Судя по всему для этой функции данные выделяются в некэшируемой памяти, отсюда низкая производительность. Небольшая проверка подтвердила эту гипотезу, так что пришло время исправить проблему. Я нашёл строку, где выделяется память, дважды щёлкнул по значению флага, и указал ноль.
Вместо примерно 7% процессорного времени функция стала потреблять около 0,7% и больше не представляла проблемы.
По итогам недели мой отчёт выглядел примерно так: «39,999 часа исследований, 0,001 часа программирования — огромный успех!»
Разработчикам обычно не нужно беспокоиться о случайном выделении некэшируемой памяти: в большинстве операционных систем эта опция не доступна в пользовательском пространстве стандартными методами. Но если вам интересно, насколько некэшируемая память способна замедлить работу программы, попробуйте флаги PAGE_NOCACHE или PAGE_WRITECOMBINE в VirtualAlloc.
0 ГиБ лучше, чем 4 ГиБ

У меня нет контактов в Western Digital, но можно с уверенностью предположить, что они исправили эту ошибку, заменив константу 0xFFFFFFFF (или −1) на ноль. Один введённый символ — и решена серьёзная проблема производительности.
(Подробнее об этом исследовании читайте в статье «Замедление Windows: изучение и идентификация»)
Наблюдения
- В обоих случаях проблема связана с кэшированием
- Решающим стало использование профилировщика для точного определения проблемы
- Если патч не проверен измерениями, то он не обязательно поможет
- Я мог бы написать о многих других таких случаях, но они либо слишком секретны, либо слишком скучны
- Правильное решение не обязательно должно быть сложным. Иногда огромное улучшение даёт небольшое изменение. Нужно только знать, в каком месте
Мне случалось оптимизировать код, расскоментив #define и путём других тривиальных изменений. Расскажите в комментариях, если у вас есть такие истории.