Главная » Хабрахабр » [Перевод] Невызванная функция замедляет программу в 5 раз

[Перевод] Невызванная функция замедляет программу в 5 раз

Замедляем Windows, часть 3: завершение процессов

пер. Автор занимается оптимизацией производительности Chrome в компании Google — прим.

Завершение процессов происходило медленно, сериализованно и блокировало системную очередь ввода, что приводило к многократным подвисаниям курсора мыши при сборке Chrome. Летом 2017 года я боролся с проблемой производительности Windows. Я рассказывал об этом в статье «24-ядерный процессор, а я не могу сдвинуть курсор». Основная причина заключалась в том, что при завершении процессов Windows тратила много времени на поиск объектов GDI, удерживая при этом критическую секцию system-global user32.

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

Причина оказалась в изменении нашего кода.
Но на самом деле баг не вернулся.

Каждый процесс Windows содержит несколько стандартных дескрипторов объектов GDI. Для процессов, которые ничего не делают с графикой, эти дескрипторы обычно имеют значение NULL. При завершении процесса Windows вызывает некоторые функции для этих дескрипторов, даже если они NULL. Это не имело значения — функции работали быстро — до выхода Windows 10 Anniversary Edition, в которой некоторые изменения в безопасности сделали эти функции медленными. Во время работы они удерживали ту же блокировку, которая использовалась для событий ввода. При одновременном завершении большого количества процессов каждый делает несколько вызовов медленной функции, которая удерживает эту критическую блокировку, что в итоге приводит к блокировке пользовательского ввода и к подвисанию курсора.

Я не знаю подробностей, но думаю, что исправление Microsoft было примерно таким: Патч Microsoft заключался в том, чтобы не вызывать эти функции для процессов без объектов GDI.

+ if (IsGUIProcess())
+ NtGdiCloseProcess();
– NtGdiCloseProcess();

То есть просто пропустить очистку GDI, если процесс не является процессом GUI/GDI.

Поскольку компиляторы и другие процессы, которые у нас быстро создаются и завершаются, не использовали объекты GDI, этого патча оказалось достаточно, чтобы исправить подвисание UI.

Оказалось, что процессам очень легко фактически выделяются некоторые стандартные объекты GDI. Если ваш процесс загружает gdi32.dll, то вы автоматически получите объекты GDI (DC, поверхности, регионы, кисти, шрифты и т.д.), нужны они вам или нет (обратите внимание, что эти стандартные объекты GDI не отображаются в Диспетчере задач среди объектов GDI для процесса).

Я имею в виду, зачем компилятору загружать gdi32.dll? Но это не должно быть проблемой. И очень легко случайно загрузить одну из этих библиотек. Ну, оказалось, что если загрузить user32.dll, shell32.dll, ole32.dll или многие другие DLL, то вы автоматически получите вдобавок gdi32.dll (с вышеупомянутыми стандартными объектами GDI).

Поскольку набор тестов LLVM генерирует очень много процессов, он в конечном итоге сериализуется при завершении процессов, вызывая огромные задержки и зависания ввода, намного хуже, чем те, что были в 2017 году. Тесты LLVM при загрузке каждого процесса вызывали CommandLineToArgvW (shell32.dll), а иногда вызывали SHGetKnownFolderPath (тоже shell32.dll) Этих вызовов оказалось достаточно, чтобы вытянуть gdi32.dll и сгенерировать эти страшные стандартные объекты GDI.

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

После этого набор тестов LLVM редко вызывал какие-либо функции из любой проблемной библиотеки DLL. Первым делом мы избавились от вызова CommandLineToArgvW, вручную отпарсив командную строку. Причина заключалась в том, что даже оставшегося условного вызова оказалось достаточно, чтобы всегда вытягивать shell32.dll, который в свою очередь вытягивал gdi32.dll, создающий стандартные объекты GDI. Но мы заранее знали, что это никак не повлияет на производительность.

Отложенная загрузка означает, что библиотека загружается по требованию — при вызове функции — вместо загрузки при запуске процесса. Вторым исправлением стала отложенная загрузка shell32.dll. Это означало, что shell32.dll и gdi32.dll будет загружаться редко, а не всегда.

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

Нужно лишь знать, где набрать «ноль». Иногда мельчайшие изменения имеют самые большие последствия.

Стоит повторить, что мы обратили внимание на код, который не выполнялся — и это стало ключевым изменением. Если у вас есть инструмент командной строки, который не обращается к gdi32.dll, то добавление кода с условным вызовом функции многократно замедлит завершение процессов, если загружается gdi32.dll. В приведённом ниже примере CommandLineToArgvW никогда не вызывается, но даже простое присутствие в коде (без задержки вызова) негативно отражается на производительности:

int main(int argc, char* argv[])
}

Так что да, удаления вызова функции, даже если код никогда не выполняется, может быть достаточно, чтобы значительно повысить производительность в некоторых случаях.
Когда я исследовал начальную ошибку, я написал программу (ProcessCreateTests), которая создавала 1000 процессов, а затем параллельно их все убивала. Это воспроизвело зависание, и когда Microsoft исправила ошибку, я использовал тестовую программу для проверки патча: см. видео. После реинкарнации бага я изменил свою программу, добавив опцию -user32, которая для каждого из тысячи тестовых процессов загружает user32.dll. Как и ожидалось, время завершения всех тестовых процессов резко возрастает с этой опцией, и легко обнаружить подвисания курсора мыши. Время создания процессов также увеличивается с параметром -user32, но во время создания процессов нет подвисаний курсора. Можете использовать эту программу и посмотреть, насколько ужасной может быть проблема. Здесь показаны некоторые типичные результаты моего четырёхъядерного/восьмипоточного ноутбука после недели аптайма. Опция -user32 увеличивает время для всего, но особенно драматично увеличивается блокировка UserCrit при завершении процессов:

448 s (2. > ProcessCreatetests.exe
Process creation took 2. 008 s total, maximum was 0. 448 ms per process).
Lock blocked for 0. 001 s.

801 s (0. Process destruction took 0. 004 s total, maximum was 0. 801 ms per process).
Lock blocked for 0. 001 s.

154 s (3. > ProcessCreatetests.exe -user32
Testing with 1000 descendant processes with user32.dll loaded.
Process creation took 3. 032 s total, maximum was 0. 154 ms per process).
Lock blocked for 0. 007 s.

240 s (2. Process destruction took 2. 991 s total, maximum was 0. 240 ms per process).
Lock blocked for 1. 864 s.

Я подумал о некоторых методах ETW, которые можно применить для более детального изучения проблемы, и уже начал писать их. Но натолкнулся на такое необъяснимое поведение, которому решил посвятить отдельную статью. Достаточно сказать, что в этом случае Windows ведёт себя ещё более странно.

Другие статьи цикла:

  • Первый отчёт о подвисаниях UI: «24-ядерный процессор, а я не могу сдвинуть курсор»
  • Следующая статья, которая подводит к пониманию проблемы: «Что *делает* Windows, удерживая эту блокировку»
  • Статья о другой блокировке UI из-за взаимодействия между воркерами Gmail, ASLR в v8, политикой выделения памяти CFG и медленным сканированием WMI: «24-ядерный CPU, а я не могу набрать электронное письмо»
  • Загрузка компилятором gdi32.dll кажется странной, но ещё более странно, что компилятор загружает mshtml.dll, что раньше делал VC++ в некоторых случаях
  • Иногда недели исследований приводят к маленьким, но критическим изменениям, как обсуждалось в статье «Знать, где набрать ноль»
  • Видео с демонстрацией использования ProcessCreateTests и ETW для проверки исправления бага
  • Первое изменение для LLVM путём мануального парсинга командной строки
  • Второе исправление для LLVM с помощью задержки загрузки shell32.dll

Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

Напишите о нас в своей газете: как IT-компании используют Pressfeed

Во-первых, это запросы, связанные с IT-тематикой, к примеру, запросы от площадки Tproger, пишущей для разработчиков. Для себя мы определили несколько интересных нам форматов запросов на Pressfeed. Такие запросы идут от отраслевых HR-сайтов: Officemaps.ru, HR-tv, Rjob.ru. Во-вторых, мы отвечаем на запросы, ...

Dell выходит на биржу и берет курс на гибридное облако

В одном из наших материалов мы говорили, что Dell планирует стать публичной компанией и впервые за пять лет разместить акции на бирже. В начале этой недели стала известна дата, когда ценные бумаги Dell выйдут в оборот, — 28 декабря. При ...