【发布时间】:2020-10-10 02:53:54
【问题描述】:
考虑这个 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。不过,该程序实际上是等价的,而且 GCC 知道这一点。作为证据,如果我使用-Os 而不是-O3 进行编译,那么char carry 和int carry 都会产生完全相同的程序集:
f:
nop # do something that sets eax and CF
jnc .L1
negl %eax
.L1:
ret
似乎两件事中的一件必须是真的,但我不确定哪一件:
-
testb比movzbl后跟testl快,因此 GCC 将后者与int一起使用是错过了优化。 -
testb比movzbl后跟testl慢,因此 GCC 将前者与char一起使用是错过了优化。
我的直觉告诉我,额外的指令会更慢,但我也有一个挥之不去的怀疑,即它是否会防止我看不到的部分寄存器停顿。
顺便说一句,xor 在setc 之前将寄存器归零的通常推荐方法在我的实际示例中不起作用。在内联汇编运行后你不能这样做,因为xor 将覆盖进位标志,你不能在内联汇编运行之前这样做,因为在the real context of this code 中,每个通用调用破坏寄存器都是已经以某种方式使用了。
【问题讨论】:
-
还有一个基于 CF 的条件否定的替代序列(比这个短),你认为这个主题吗?
-
@harold This one?
-
这也不错,但我想到的是
sbb / xor / sub -
@harold 弄清楚哪些操作数去哪里进行这项工作是一项很好的大脑练习(看起来像
sbb %ecx, %ecx;xor %ecx, %eax;sub %ecx, %eax)。无论如何,是的,我肯定在寻找那些聪明的想法。我想知道处理器是否足够聪明,可以避免对ecx的错误依赖。 -
AMD 的处理器是(根据 Agner Fog 的说法),据我所知,英特尔仍在读取 ecx
标签: performance assembly x86 x86-64 micro-optimization