Рассмотрим код C:
int f(void) {
int ret;
char carry;
__asm__(
"nop # do something that sets eax and CF"
: "=a"(ret), "=@ccc"(carry)
);
return carry ? -ret : ret;
}
Когда я компилирую его с помощью gcc -O3
, я получаю следующее:
f:
nop # do something that sets eax and CF
setc %cl
movl %eax, %edx
negl %edx
testb %cl, %cl
cmovne %edx, %eax
ret
Если я изменю char carry
на int carry
, Вместо этого я получаю следующее:
f:
nop # do something that sets eax and CF
setc %cl
movl %eax, %edx
movzbl %cl, %ecx
negl %edx
testl %ecx, %ecx
cmovne %edx, %eax
ret
Это изменение заменило testb %cl, %cl
на movzbl %cl, %ecx
и testl %ecx, %ecx
. Программа фактически эквивалентна, и G CC знает об этом. В качестве доказательства этого, если я компилирую с -Os
вместо -O3
, то оба char carry
и int carry
приведут к одной и той же сборке:
f:
nop # do something that sets eax and CF
jnc .L1
negl %eax
.L1:
ret
Это похоже на одно из двух должно быть правдой, но я не уверен, что:
- A
testb
быстрее, чем movzbl
, за которым следует testl
, поэтому G CC использует последнее с int
- это пропущенная оптимизация. - A
testb
медленнее, чем movzbl
, за которым следует testl
, поэтому использование первого G CC с char
является пропущенная оптимизация.
Моя интуиция подсказывает мне, что дополнительная инструкция будет медленнее, но у меня также есть мучительные сомнения, что это предотвращает частичную остановку регистра, которую я просто не вижу.
Между прочим, обычный рекомендуемый подход xor
обнулить регистр перед setc
не работает в моем реальном примере. Вы не можете сделать это после запуска встроенной сборки, поскольку xor
перезапишет флаг переноса, и вы не можете сделать это до запуска встроенной сборки, поскольку в реальный контекст этого кода , каждый регистр общего назначения, закрывающий вызовы, каким-то образом уже используется.