Вы правы, очевидная реализация использует ветки. Моя первоначальная идея состояла в том, чтобы преобразовать каждый операнд в 0 или -1 с помощью сдвига знака, расширяющего знак, а затем с помощью побитовой инструкции и инструкции, но gcc (1) напомнил мне о наборе xy x86 инструкция, так что кажется проще.
Следующая функция реализует: int f(int a, int b) { return a && b; }
.text
.globl f
f:
cmpl $0, 4(%esp)
setne %dl
cmpl $0, 8(%esp)
setne %al
movzbl %al, %eax
andl %edx, %eax
ret
Как оказалось, правильное значение для выражения можно вычислить без переходов или переходов, но, определяя мою реализацию как функцию, я обманываю, потому что она заставляет оба операнда вычисляться в чтобы выполнить вызов функции, после чего функция не наносит вреда, считывая оба операнда без короткого замыкания, поскольку известно, что параметры являются копиями.
Но для открытого кода то, можно ли это сделать без перехода, полностью зависит от того, можно ли вычислить правый операнд без побочных эффектов. Итак, это зависит от типа. Представьте себе случай, когда правым операндом является вызов функции, возможно, операция ввода-вывода или системный вызов ядра. Сгенерированный код не может оценить его вообще , если левый операнд имеет значение false, и я не вижу, как это можно сделать без перехода или перехода.
Интересно, что gcc -O (который генерирует подобный код), кажется, нарушает C99 6.5.13 (4), который категорически заявляет «Если первый операнд сравнивается равным 0, второй операнд не оценивается». В случае сборки f () он, очевидно, решает, что, поскольку у параметра значения не может быть побочных эффектов, компиляция кода не будет вредной, вопреки буквальной спецификации C99.