Условное перемещение (cmov / csel)
- title
- Условное перемещение (cmov / csel)
- type
- concept
- summary
- Беспереходная инструкция, выбирающая значение по флагу; быстрее ветвления только при непредсказуемом переходе
- tags
- performance, microarchitecture, compilers
- sources
- lucky-code
- created
- 2026-07-18
- updated
- 2026-07-22
- lang
- ru
- translation_of
- conditional-move
- source_updated
- 2026-07-22
- translated
- 2026-09-01
- translator
- lllm/antigravity/gemini-3.7-flash-medium
Условное перемещение записывает в приёмник одно из двух исходных значений в зависимости от флага условия, без ветвления. В x86 оно называется cmov, в AArch64 - csel (conditional select). Вместо того чтобы перепрыгивать через запись, процессор вычисляет оба входных значения-кандидата, а затем инструкция фиксирует то, которое выбрано флагом. Поток управления остаётся линейным - через цикл проходит ровно один путь.
Компромисс
Условное перемещение не даёт ускорения даром. Оно меняет потенциальный сброс конвейера на гарантированный объём работы. Правильно предсказанное ветвление почти ничего не стоит: процессор спекулятивно идёт дальше, и спекуляция подтверждается. Условное перемещение всегда выполняет одну и ту же работу независимо от флага, поэтому в нём не бывает ошибок предсказания, но оно и никогда не получает скидки за пропуск второй ветки, которую даёт хорошо предсказанный переход.
Поэтому всё решает предсказуемость:
- Если переход легко предсказать (он почти всегда выполняется или почти всегда нет), ветвление быстрее: предсказатель угадывает почти всегда, и переход практически бесплатен.
- Если переход непредсказуем, как бросок монеты, условное перемещение быстрее: реальное ветвление ошибалось бы примерно в половине случаев, а каждый промах сбрасывает конвейер и заново запрашивает инструкции.
Разбиение в быстрой сортировке на случайных данных близко к худшему сценарию для предсказателя переходов: около половины элементов оказывается по обе стороны от опорного элемента без какой-либо закономерности. Именно здесь беспереходный вариант себя оправдывает. См. compiler-codegen-luck - пример, где один и тот же цикл разбиения работал в 6 раз быстрее, как только Clang сгенерировал csel вместо ветвления.
Кто принимает решение
Компилятор выбирает между ветвлением и условным перемещением на этапе компиляции, когда распределение данных ещё неизвестно, поэтому он действует наугад. Этот выбор к тому же хрупок: в LLVM он зависит от прохода SimplifyCFG, сопоставляющего определённую форму IR, и мелкие, семантически несущественные различия в исходном коде могут изменить решение на противоположное. GCC и Clang на одном и том же коде часто принимают разные решения.
Если заранее известно, что переход действительно непредсказуем, компилятор можно подтолкнуть. В Clang есть __builtin_unpredictable(), чтобы пометить условие как случайное. Тернарный оператор (p = cond ? a : b) часто транслируется в условное перемещение, но гарантии нет. Когда это важно, надёжный способ проверки - смотреть в сгенерированный ассемблер, а не полагаться на структуру исходного кода. Более глубокая причина сложности: польза от условного перемещения зависит от данных во время выполнения, которые исходный код выразить не может, так что никакие ухищрения на уровне исходников не решают вопрос окончательно.
Условный выбор - лишь один пункт в гораздо более обширном каталоге беспереходных целочисленных трюков: арифметика с насыщением, извлечение знака, min/max без сравнения и перехода, деление на константу. Все они собраны в книге Уоррена hackers-delight, и её чтение - самый быстрый способ распознавать конструкции, к которым компилятор прибегает, когда выдаёт неожиданный код.