Для некоторых оптимизаций требуются сложные структуры данных и тысячи строк кода. В других же случаях серьёзный прирост производительности даёт минимальное изменение: иногда нужно лишь поставить ноль. Это похоже на старую байку о котельщике, который знает правильное место для удара молотком, а потом выставляет клиенту счёт: $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)


  1. Shpiler
    23.12.2018 14:17
    +4

    Буквально вчера был эпизод увеличения производительности в ~72 раза одной кнопкой. Это было delete по вызову логирования, который по некоторым историческим причинам попал внутрь одной функции, вызываемой при численном интегрировании сложной матмодели. Профилировщик, правда, для этого не понадобился, просто нужно помнить что логи — штука медленная, и если пишешь их для отладки, то после себя — удалять, а не комментировать на случай «а вдруг оно опять понадобится». Не понадобится. Зато кто-нибудь другой может залезть в код и сказать «о! логи закоменчены!» и вернуть всё взад.


    1. third112
      24.12.2018 10:06

      А можно добавить еще один коммент: " логи — штука медленная!"? :)


  1. walti
    23.12.2018 15:18

    Почему Microsoft просто не поставили более мощный процессор в приставку?
    Ведь разработка нового процессора стоит меньше, чем зарплата программиста.


    1. Sultansoy
      23.12.2018 16:00
      +5

      А ведь именно из-за того мышления мы сейчас получаем лагающие игры даже на новых i9 в связке с 2080.
      Код должен быть оптимизирован всегда. А херовый код даже лучший процессор не вытянет.


      1. mapron
        23.12.2018 16:43
        +1

        Да нет, просто подтянулись разработчики на Electron =)


      1. timdorohin
        23.12.2018 17:48
        +8

        Я искренне подозреваю что сказанное про процессор было сарказмом, просто его не поняли. Ведь разработка процессора намного дороже чем найти баг в программе.


        1. walti
          23.12.2018 19:38

          разработка
          Нет, это было сказано полностью серьезно, конечно-же.


          1. dimonoid
            23.12.2018 21:43

            Может имелась ввиду не разработка, а покупка процессора?


            1. walti
              23.12.2018 21:56
              +1

              walti

              Ведь разработка нового процессора стоит меньше, чем зарплата программиста.

              walti
              разработка
              Нет, это было сказано полностью серьезно, конечно-же.

              dimonoid
              Может имелась ввиду не разработка, а покупка процессора?


              Знаете, я уже не уверен, что я имел ввиду. (с) walti


      1. Druu
        24.12.2018 06:46

        Код должен

        Кому должен?


    1. JediPhilosopher
      23.12.2018 22:07
      +3

      У приставок своя атмосфера. В отличие от настольных ПК они не подлежат апгрейду и имеют при этом гораздо более долгий срок жизни. Поэтому в любом случае даже супер-пупер железо на момент выхода приставки (а точнее на момент начала ее разработки) уже через несколько лет успеет устареть и разработчикам придется выкручиваться.

      Я помню очень удивлялся, как на PS3 с ее жалкими 512 (кажется) Мб памяти летали игры, которые лагали на ПК с двумя гигами. Сколько ухищрений, оптимизаций и упрощений запихивали разработчики туда.


      1. naneri
        24.12.2018 00:31
        +1

        «PS3 имеет 256 Мб XDR DRAM оперативной памяти производства Rambus.»

        Wikipedia


        1. khim
          24.12.2018 08:02

          Там ещё столько же видеопамяти, а когда очень надо — что-то можно и в видеопамять сложить… прям как tor на машинках 30-летней давности, да…


  1. questor
    23.12.2018 15:54

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


    1. blind_oracle
      26.12.2018 13:43

      There are only two hard things in Computer Science: cache invalidation and naming things.

      — Phil Karlton


      1. vesper-bot
        26.12.2018 14:14

        Забыли про ошибку на единицу.


  1. simik2
    23.12.2018 18:33

    Сильно увеличил размер индекса в чей-то реализации хэш-таблицы. На малом кол-ве элементов хэш работал нормально, а на миллионах вставал колом.


  1. xytop
    23.12.2018 20:58

    Исправил регулярное выражение, тем самым увеличив скорость комплиляции шаблонов Laravel в 40 раз :)


    1. 411
      24.12.2018 06:33

      Тоже была подобная проблема с регулярками, только для проверки на то, что строка является ссылкой(целиком). Правда решил не исправлением большой регулярки, а проверкой каждой части ссылки своей регуляркой.


      1. faiwer
        24.12.2018 07:27

        Лет 5 назад столкнулся с интересной проблемой catastrophic backtracing в regular expressions (статья не моя, просто пример). Это когда визуально вроде всё нормально, и оно даже работает. Но с увеличением длины строки производительность гасится весьма нелинейно. Патч в 1-2 символа. С тех пор стал писать их куда внимательнее :)


    1. wickedweasel
      24.12.2018 13:38

      Ускорил файловый кэш (на секундочку, сам кэш призван ускорять) в бородатом симфони 1.2 в 3-10 раз


  1. dimonoid
    23.12.2018 21:32
    +1

    Нужно просто разрабатывать на процессоре (пусть даже i9), но на пониженной частоте строго 2-3ghz, а запускать потребителю — на нормальных 4-5ghz. Вот и весь секрет скорости.


    1. vesper-bot
      24.12.2018 11:29
      -1

      Не разрабатывать, а отлаживать (тестировать). И желательно частоту вообще где-то в 1ГГц загнать, но заставить ПО использовать все доступные ядра, чтобы ещё и многопоточные ошибки проявлялись почаще.


  1. cynovg
    23.12.2018 22:47

    Кто-то закомментировал кеширование при формировании списка товаров для отладки и забыл вернуть. Исправил это спустя год-полтора :-)


  1. Goodkat
    24.12.2018 00:55

    Добавил в таблицу индекс, отчёт стал готовиться за 40 секунд вместо 40 минут.


  1. 0xf0x
    24.12.2018 01:16

    У нас один добрый человек выводил hp-bar'ы над юнитами, используя stencil bufffer, очищая его каждый раз перед отрисовкой нового bar'a. Простая замена на scirrors дала более чем двукратный прирост fps:)


  1. Azrael33
    24.12.2018 10:32

    >> Добавил в таблицу индекс…

    Удалил сложный индекс в таблице-снизил число блокировок и увеличил производительность БД


  1. nmrulin
    24.12.2018 11:55

    Одни из самых противных ошибок это те которые не приводят напрямую к вылету, но зато грузят процессор.


  1. achekalin
    24.12.2018 12:12

    «Байка о котельщике» — понятно, что перевод, но все же её в России про Капицу рассказывают, а не про котельщика.


    1. SergeyMax
      24.12.2018 12:36
      +1

      Ой да ладно, эта байка пересказывается с абсолютно рандомными именами, фамилиями и местами событий.


      1. achekalin
        24.12.2018 12:38

        Но вот про котельщика я ее не слышал, причем на историю про котельщика автор оригинала ссылается как на устойчивую единицу рассказа.


        1. SergeyMax
          24.12.2018 12:40
          +1

          А я про Капицу не слышал, зато слышал про инженера Уатта и автомеханика (это два разных варианта).


          1. achekalin
            24.12.2018 13:33

            Ну, кто что читал. Но идея про Капицу мне для легенды больше нравится.


    1. ads83
      24.12.2018 12:59

      А я читал версию, что на атомной станции были проблемы. Мелкие, но неприятные, и это атомная станция. И как старенький профессор пометил карандашом проблемную трубу, оказался тысячу раз прав, и в конце рассказа объяснял, что взял деньги не за время, потраченное на решение проблемы, а за знание, что и где смотреть
      P.S. Читал давно, помню только общий сюжет и найти сходу не смог :(


      1. wickedweasel
        24.12.2018 13:36

        Ага, фрилансеры чинят атомную станцию.


        1. AlexanderG
          24.12.2018 17:57

          А про забытые в первом контуре ключи и даже фуфайки, к сожалению, не шутки (с мертвого форума):

          — А откуда вообще мусор то? Ну поменяли задвижку ну и что?
          (случайно забытые гаечные ключи, болты-гайки, ставшие «лишними» я в расчет не беру).
          — Так дело в том, что чаще всего именно ключи, гайки и т.д. оттуда и выносятся. Это что, бывало вылавливали фуфайки на фильтрах РГК — почти вход в активную зону.
          — Не, это, конечно, мусор, но я думал что такой мусор (болты, ключи) исключен в принципе.Я понимаю что моя наивность не знает границ


  1. mdaemon
    24.12.2018 14:12

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


    1. AlexanderG
      24.12.2018 17:57

      Практически Flyweight pattern