Перейти к основному содержимому

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 / SETNGEA < B (знаковое)
SETG / SETNLEA > B (знаковое)
SETB / SETNAEA < B (беззнаковое)
SETA / SETNBEA > 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 при вызове функций — если ветка вызывает разный код, нужен переход, а не условное копирование.