movzbl 后接 testl 是否比 testb 快?
Is movzbl followed by testl faster than testb?
考虑这个 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
看来两件事中的一件必须是真的,但我不确定是哪一件:
- A
testb
比 movzbl
后跟 testl
快,因此 GCC 将后者与 int
一起使用是一个错过的优化。
- A
testb
比 movzbl
后跟 testl
慢,因此 GCC 将前者与 char
一起使用是错过了优化。
我的直觉告诉我,额外的指令会变慢,但我也有一个挥之不去的疑问,即它是否会阻止我看不到的部分寄存器停顿。
顺便说一句,在我的真实示例中,通常推荐的 xor
在 setc
之前将寄存器清零的方法不起作用。你不能在内联汇编运行之后这样做,因为 xor
会覆盖进位标志,你不能在内联汇编运行之前这样做,因为在 the real context of this code 中,每个通用调用-被破坏的寄存器已经以某种方式被使用了。
据我所知,使用 test
与 movzb
.
读取字节寄存器没有任何缺点
如果您要进行零扩展,那么在 asm 语句之前不对 reg 进行异或零操作也是一个错过的优化,并且 setc
进入其中,因此零扩展的成本不在关键路径。 (在 Intel IvyBridge+ 以外的 CPU 上 movzx r32, r8
不是零延迟)。当然,假设有免费注册。最近的 GCC 有时确实发现了这种 zero/set-flags/setcc 优化,用于从标志设置指令生成 32 位布尔值,但当事情变得复杂时经常会错过它。
对你来说幸运的是,你的实际用例无论如何都无法进行优化(除了 mov [=14=], %eax
归零,这将偏离延迟的关键路径,但会导致 Intel P6 上的部分寄存器停顿family, and cost more code size.) 但它仍然是您的测试用例的优化失误。
考虑这个 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
看来两件事中的一件必须是真的,但我不确定是哪一件:
- A
testb
比movzbl
后跟testl
快,因此 GCC 将后者与int
一起使用是一个错过的优化。 - A
testb
比movzbl
后跟testl
慢,因此 GCC 将前者与char
一起使用是错过了优化。
我的直觉告诉我,额外的指令会变慢,但我也有一个挥之不去的疑问,即它是否会阻止我看不到的部分寄存器停顿。
顺便说一句,在我的真实示例中,通常推荐的 xor
在 setc
之前将寄存器清零的方法不起作用。你不能在内联汇编运行之后这样做,因为 xor
会覆盖进位标志,你不能在内联汇编运行之前这样做,因为在 the real context of this code 中,每个通用调用-被破坏的寄存器已经以某种方式被使用了。
据我所知,使用 test
与 movzb
.
如果您要进行零扩展,那么在 asm 语句之前不对 reg 进行异或零操作也是一个错过的优化,并且 setc
进入其中,因此零扩展的成本不在关键路径。 (在 Intel IvyBridge+ 以外的 CPU 上 movzx r32, r8
不是零延迟)。当然,假设有免费注册。最近的 GCC 有时确实发现了这种 zero/set-flags/setcc 优化,用于从标志设置指令生成 32 位布尔值,但当事情变得复杂时经常会错过它。
对你来说幸运的是,你的实际用例无论如何都无法进行优化(除了 mov [=14=], %eax
归零,这将偏离延迟的关键路径,但会导致 Intel P6 上的部分寄存器停顿family, and cost more code size.) 但它仍然是您的测试用例的优化失误。