SETcc, CMOV и ветвления без прыжков
Контекст: x86/x86-64, синтаксис Intel (NASM). См. управляющие конструкции про флаги и
Jcc.
Два способа реагировать на флаги
После CMP или арифметики процессор выставляет ZF, CF, SF, OF и др. Классический путь — условный переход (JE, JG, JB …): меняется RIP, конвейер может "промахнуться" по предсказанию ветвления.
Альтернативы на x86:
SETcc— записать в 8-битный регистр или байт памяти 0 или 1 по условию.CMOVcc— условно скопировать значение между регистрами, не меняя поток команд.
Обе группы читают те же условия, что и Jcc (E = equal/ZF, NE, L/G знаковые, B/A беззнаковые).
SETcc — флаг в байт
cmp rax, rbx
sete al ; al = 1, если rax == rbx, иначе 0
movzx rax, al ; расширить до 64 бит, если нужно целое 0/1
Разбор:
cmp rax, rbxвычисляет разность для установки флагов, не меняя сами операнды.sete alсмотрит наZF: при равенстве записывает1, иначе0.- Результат
SETccвсегда 8-битный, поэтому используется регистрAL. movzx rax, alрасширяет байт до полноценного 64-битного значения0или1.- Такой шаблон превращает условие во "флаг-число", которое удобно хранить, суммировать и передавать дальше.
Частые мнемоники:
| Мнемоника | Условие (после cmp A,B) |
|---|---|
SETE / SETZ | равны |
SETNE / SETNZ | не равны |
SETL / SETNGE | A < B (знаковое) |
SETG / SETNLE | A > B (знаковое) |
SETB / SETNAE | A < B (беззнаковое) |
SETA / SETNBE | A > B (беззнаковое) |
Ограничение: операнд назначения — только 8 бит (AL, BL, байт в памяти). Для 32/64-битного 0/1 делают SETcc + MOVZX/MOVSX.
Применение — упаковка логического результата в структуру, подготовка маски, минимизация ветвлений при серии мелких сравнений.
Проверка чётности без перехода в ветку:
mov rax, rdi
and al, 1 ; оставить только младший бит
setz al ; al = 1, если число чётное
movzx rax, al ; rax = 0 или 1
Разбор:
and al, 1изолирует младший бит: у чётных он 0, у нечётных 1.setz alсмотрит наZFпослеAND: нулевой младший бит → чётное →AL=1.movzx rax, alрасширяет байт до 64 бит для дальнейшей арифметики.- Вместо
test/jmpполучаем компактный флаг0/1, удобный для массивов признаков. TESTвместоANDтоже подошёл бы, если результат побитовой маски не нужен.
min для беззнаковых через CMOV:
mov rax, rdi
cmp rdi, rsi
cmovb rax, rsi ; если rdi < rsi (unsigned), rax := rsi
Разбор:
cmovb— "below", беззнаковый аналог "меньше".- Для знаковых чисел здесь нужен
cmovl, иначе семантика будет неверной на отрицательных значениях. - Перед
CMOVвRAXуже лежит одно из значений — это обязательный шаблон. - Подходит для короткого выбора между двумя регистрами без меток.
CMOVcc — условный MOV между регистрами
cmp rdi, rsi
mov rax, rdi
cmovl rax, rsi ; если rdi < rsi (знаковое), rax := rsi
Разбор:
cmp rdi, rsiвыставляет флаги для знакового сравнения двух значений.mov rax, rdiподготавливает значение по умолчанию (ветка без замены).cmovl rax, rsiвыполняет копирование только если условие "меньше" истинно (SF != OF).- Поток исполнения остаётся линейным: нет прыжка и нет смены адреса исполнения.
- Фрагмент реализует условный выбор значения без разветвления кода.
CMOV не допускает память в обоих операндах (только регистр ↔ регистр в типичных формах). Условие ложно — значение в приёмнике не меняется, поэтому перед CMOV часто копируют "значение по умолчанию":
mov rax, rdi ; предположим "максимум = rdi"
cmp rdi, rsi
cmovl rax, rsi ; если rsi больше — взять rsi
Разбор:
- Начальное
mov rax, rdiфиксирует кандидат на максимум до проверки условия. cmp rdi, rsiподготавливает флаги для знакового сравнения.cmovl rax, rsiзаменяет результат наrsi, если первый аргумент меньше второго.- Если условие ложно,
raxостаётся равнымrdiбез отдельной веткиelse. - Это классическая branchless-реализация
max(a, b)для целых со знаком.
Так реализуют max(a,b) и min(a,b) без двух меток и прыжков.
Когда что выбирать
| Ситуация | Подход |
|---|---|
| Большие разные ветки кода | Jcc — меньше инструкций |
| Короткая выборка между двумя значениями | CMOV |
| Нужен 0/1 в памяти или массив флагов | SETcc |
| Глубокий конвейер, непредсказуемые ветви | иногда выгоднее CMOV (зависит от CPU и профиля) |
| Код, критичный к побочным эффектам чтения | осторожно: CMOV всё равно выполняет оба операнда с точки зрения микроархитектуры в старых моделях; для памяти с побочными эффектами используйте ветвление |
Современные компиляторы генерируют CMOV для тернарного оператора a < b ? b : a на простых типах.
Связь с длинной арифметикой
В длинном сложении финальный перенос за старший разряд удобно сохранить так:
setc al
movzx rax, al ; 1 = было переполнение беззнакового сложения
Разбор:
setc alчитает флаг переносаCF, установленный предыдущей арифметической операцией.- Если перенос был, в
ALзаписывается1, иначе0. movzx rax, alнормализует результат в стандартный 64-битный вид для дальнейших вычислений.- Шаблон удобен после длинного
adc-цикла, когда нужно явно вернуть статус переполнения.
Вместо перехода на метку "overflow".
Чего избегать
SETccв 32-битный регистр напрямую — такой формы нет; только 8 бит.- Путаница знакового и беззнакового условия — те же правила, что для
JLиJBв типах данных. - CMOV вместо Jcc при вызове функций — если ветка вызывает разный код, нужен переход, а не условное копирование.