Search
Write a publication
Pull to refresh

Как замедлить программу и почему это может быть полезно?

Level of difficultyEasy
Reading time6 min
Views2.6K
Original author: Stefan Marr

Основная часть исследований производительности языков программирования задаётся разными вариациями единственного вопроса: как нам ускорить некую конкретную программу? Реже мы исследуем, как она может использовать меньше памяти. Это означает, что подавляющая масса исследований сосредоточена исключительно на снижении объёма ресурсов, необходимых для выполнения какой-то вычислительной задачи.

Тогда почему нас может интересовать, как замедлять программы?

Замедление программ оказывается на удивление полезным занятием!

Замедление программ может быть полезно при поиске состояний гонки, симуляции ускорения и оценке точности профилировщиков.

Для выявления состояний гонки можно использовать подход, схожий с фаззингом. Вместо того, чтобы изучать реализацию программы, видоизменяя её входные данные, мы можем изучать различные переплетения команд, потоков или диспетчеризации событий, замедляя части программы с целью изменения таймингов. Такой подход позволяет нам идентифицировать баги конкурентности; он применяется в CHESSWAFFLE и NACD.

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

И, как говорилось выше, замедление также можно использовать для оценки точности профилировщиков, но это уже тема для отдельного поста.

Впрочем, современные решения по замедлению программ для таких сценариев использования достаточно грубы. При распознавании состояний гонки часто применяется адаптированный планировщик или, например, API наподобие Thread.sleep(). Coz приостанавливает исполнение всех остальных потоков. В работе, посвящённой измерению того, дают ли профилировщики актуальные результаты, в программы на Java вставляются байт-коды для вычисления чисел Фибоначчи.

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

Какие команды x86 позволяют согласованно замедлять базовые блоки?

Допустим, программа выполняется на каком-то процессоре x86, и мы изучаем программы с точки зрения процессоров.

При выполнении бенчмарка наподобие Towers виртуальная машина HotSpot платформы OpenJDK может компилировать его в команды x86 следующим образом:

mov dword ptr [rsp+0x18], r8d
mov dword ptr [rsp], ecx
mov qword ptr [rsp+0x20], rsi
mov ebx, dword ptr [rsi+0x10]
mov r9d, edx
cmp edx, 0x1
jnz 0x... <Block 55>

Это один из базовых блоков, генерируемых компилятором C2 HotSpot. Для наших целей достаточно обеспечить операции доступа к памяти при помощи команд mov, а затем проверить, содержит ли регистр edx значение 1. Если это не так, то мы переходим к Block 55. В противном случае исполнение продолжается в следующем базовом блоке. Важнейшее свойство базового блока заключается в том, что внутри него отсутствует поток управления, то есть после начала его исполнения все его команды будут исполнены.

Однако как нам всё это замедлить?

В архитектуре x86 есть множество команд, которые можно попробовать вставить в блок, и каждый их них, вероятно, будет потреблять такты CPU. Однако современные CPU стремятся одновременно выполнять максимально возможное количество команд при помощи внеочередного исполнения (out-of-order execution). Это означает, что те команды в нашем базовом блоке, которые не зависят напрямую друг от друга, могут быть исполнены одновременно. Например, первые три команды mov получают доступ не к одному регистру и не к одному блоку памяти, поэтому порядок их исполнения не важен. Однако применяемые CPU оптимизации зависят от программы и поколения конкретного CPU или, скорее, от его микроархитектуры.

Чтобы найти команды, подходящие для замедления базовых блоков, мы экспериментировали только на CPU Intel Core i5-10600, имеющем микроархитектуру Comet Lake-S. На других микроархитектурах ситуация может сильно отличаться.

Для получения нужного нам замедления на Comet Lake-S можно использовать команды nop или mov regX, regX. Команда mov копирует значение из регистра X в само себя, то есть, по сути, ничего не делает. Эти две команды обеспечивают замедление настолько малое, что позволяют замедлять большинство блоков точно до необходимой скорости; при этом, похоже, замедление влияет только на тот блок, для которого оно предназначалось.

Следовательно, в показанный выше базовый блок можно было бы, возможно, вставить после каждой команды команду nop. На практике же, количество команд, которые нужно вставить, зависит от времени, затрачиваемого программой на базовый блок. Однако выглядеть это может примерно так:

mov dword ptr [rsp+0x18], r8d
nop
mov dword ptr [rsp], ecx
nop
mov qword ptr [rsp+0x20], rsi
nop
mov ebx, dword ptr [rsi+0x10]
nop
mov r9d, edx
nop
cmp edx, 0x1
nop
jnz 0x... <Block 55>

Чтобы лучше понять, как Comet Lake-S обрабатывает эти команды, мы попробовали шесть кандидатов, в том числе последовательность push-pop. Подробнее о том, что и как мы пробовали, можно прочитать в нашей короткой статье, которую мы представим на воркшопе VMIL.

При вставке этих команд в базовые блоки так, чтобы исполнение каждого отдельного базового блока занимало примерно вдвое больше времени, мы создали программу, которая действительно суммарно была вдвое медленнее. Более того, при исследовании бенчмарка Towers профайлером HotSpot async-profiler и сравнении долей времени исполнения, связанных с каждым методом, выяснилось, что замедленная и обычная версии совпадают почти идеально, как видно на показанных ниже графиках. Для других кандидатов картина была иной.

График рассеяния для команд замедления с медианным процентом времени исполнения для шести верхних методов Java в Towers. Диагональ X=Y показывает, что процент времени исполнения метода остаётся одинаковым при обычном и замедленном исполнении.
График рассеяния для команд замедления с медианным процентом времени исполнения для шести верхних методов Java в Towers. Диагональ X=Y показывает, что процент времени исполнения метода остаётся одинаковым при обычном и замедленном исполнении.

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

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

А пока оставлю ниже ссылку на статью; вопросы, указания и предложения можно присылать в MastodonBlueSky или Twitter.

Абстракт

Замедление исполнения программ имеет на удивление разнообразные сценарии использования: оно помогает находить состояния гонки, позволяет оценивать ускорение и определять точность профилировщиков. Однако замедление программы — это сложная тема, потому что современные CPU и системы сред исполнения способны оптимизировать исполнение на лету, из-за чего нам сложнее сохранять поведение программы с целью избегания перекосов.

Мы протестировали шесть кандидатов-команд x86 на управляемое и детализированное замедление, в том числе NOP, MOV и PAUSE. Мы тестировали способность каждого кандидата к достижению оверхеда в 100%, с целью сохранения наблюдаемого в профилировщике поведения, а также изучали, влияет ли на результаты местоположение команд замедления внутри базовых блоков. Исходя из наших опытов на Intel Core i5-10600, мы сделали вывод, что для этих целей подходят только команды NOP и MOV. Мы считаем, что эти эксперименты могут стать основой для будущих исследований более совершенного инструментария разработчика, использующего точное замедление программ на уровне машинного кода.

  • Evaluating Candidate Instructions for Reliable Program Slowdown at the Compiler Level: Towards Supporting Fine-Grained Slowdown for Advanced Developer ToolingH. Burchell, S. Marr; In Proceedings of the 17th ACM SIGPLAN International Workshop on Virtual Machines and Intermediate Languages, VMIL'25, p. 8, ACM, 2025.

  • Статья: PDF

  • DOI: 10.1145/3759548.3763374

Bibtex

@inproceedings{Burchell:2025:SlowCandidates, abstract = {Slowing down programs has surprisingly many use cases: it helps finding race conditions, enables speedup estimation, and allows us to assess a profiler's accuracy. Yet, slowing down a program is complicated because today's CPUs and runtime systems can optimize execution on the fly, making it challenging to preserve a program's performance behavior to avoid introducing bias. We evaluate six x86 instruction candidates for controlled and fine-grained slowdown including NOP, MOV, and PAUSE. We tested each candidate’s ability to achieve an overhead of 100%, to maintain the profiler-observable performance behavior, and whether slowdown placement within basic blocks influences results. On an Intel Core i5-10600, our experiments suggest that only NOP and MOV instructions are suitable. We believe these experiments can guide future research on advanced developer tooling that utilizes fine-granular slowdown at the machine-code level.}, author = {Burchell, Humphrey and Marr, Stefan}, booktitle = {Proceedings of the 17th ACM SIGPLAN International Workshop on Virtual Machines and Intermediate Languages}, doi = {10.1145/3759548.3763374}, isbn = {979-8-4007-2164-9/2025/10}, keywords = {Benchmarking HotSpot ISA Instructions Java MeMyPublication assembly evaluation myown slowdown x86}, location = {Singapore}, month = oct, pages = {8}, pdf = {https://stefan-marr.de/downloads/vmil25-burchell-marr-evaluating-candidate-instructions-for-reliable-program-slowdown-at-the-compiler-level.pdf}, publisher = {{ACM}}, series = {VMIL'25}, title = {{Evaluating Candidate Instructions for Reliable Program Slowdown at the Compiler Level: Towards Supporting Fine-Grained Slowdown for Advanced Developer Tooling}}, year = {2025}, month_numeric = {10} }

Tags:
Hubs:
+11
Comments0

Articles