x86 ASM:无用的条件跳转?
x86 ASM: useless conditional jump?
我正在查看以下 x86 汇编代码(Intel 语法):
movzx eax, al
and eax, 3
cmp eax, 3
ja loc_6BE9A0
据我了解,这应该等于 C:
中的类似内容
eax &= 0xFF;
eax &= 3;
if (eax > 3)
loc_6BE9A0();
这似乎没有多大意义,因为这个条件永远不会为真(因为 eax
如果之前用 3 进行运算,则永远不会大于 3)。我是不是遗漏了什么,或者这真的只是一个不必要的条件?
而且:movzx eax, al
也不是必需的,如果它在那之后立即与 3 进行运算,对吗?
我问这个是因为我不太熟悉汇编语言,所以我不确定我是否遗漏了什么。
您是对的:鉴于以下 and
,movzx
是多余的。它可能是由非优化编译器生成的。
是的,如果此代码直接执行,则永远不会执行 ja
跳转。但是,如果其他地方有直接跳转到 cmp
(甚至 ja
)的代码,cmp/ja
可能不会完全没用。
这是多余的,不是您在优化的 asm 中看到的东西。
即使 cmp/ja
可能是来自其他地方的跳转目标,现有的优化编译器(如 GCC、clang、MSVC 和 ICC)也会(我很确定)执行 jmp
或不同的代码布局,而不是让执行落入始终为假的条件分支。优化器会知道这条执行路径不需要条件分支,因此会确保它没有遇到条件分支。 (即使这会额外花费 jmp
。)
这可能是一个不错的选择,即使在这种方式可以节省一些代码大小的假设情况下也是如此,因为您不想用不必要的条件分支污染/稀释分支预测历史,并且分支可以预测错误。
但是在调试模式下,一些编译器比其他编译器更能够关闭他们的大脑来优化单个语句或表达式。 (Across statements they'd always spill/reload vars to memory, 除非你使用 register int foo;
)
我能够欺骗 clang -O0
和 MSVC 发出准确的指令序列。还有类似的东西,但 GCC 更糟糕。
(令人惊讶的是,gcc -O0
仍然在单个表达式中进行一些优化,例如对 x /= 10;
使用乘法逆,并为 if(false)
删除死代码。与 MSVC 实际上将 0 放入寄存器和测试它是 0。)
void dummy();
unsigned char set_al();
int foo(void) {
if ((set_al() & 3) <= 3U)
dummy();
return 0;
}
clang12.0 for x86-64 Linux (on Godbolt)
push rbp
mov rbp, rsp
call set_al()
movzx eax, al # The redundant sequence
and eax, 3
cmp eax, 3
ja .LBB0_2
call dummy()
.LBB0_2:
xor eax, eax
pop rbp
ret
MSVC 包含相同的序列。 GCC10.3 类似但更糟,在寄存器中实现布尔值并 test
ing 它。 (两者也在同一个神箭link)
## GCC10.3
... set up RBP as a frame pointer
movzx eax, al # The redundant sequence
and eax, 3
cmp eax, 3
setbe al
test al, al # even worse than just jnbe
je .L2
call dummy()
.L2:
mov eax, 0
pop rbp
ret
由于 char
来自内存而不是 return 值,GCC 即使在调试模式下也确实优化了比较:
int bar(unsigned char *p) {
if ((*p & 3) <= 3U)
dummy();
return 0;
}
# GCC 10.3 -O0
bar(unsigned char*):
push rbp
mov rbp, rsp
sub rsp, 16 # space to spill the function arg
mov QWORD PTR [rbp-8], rdi
call dummy() # unconditional call
mov eax, 0
leave
ret
clang 和 MSVC 做测试,都用 asm like
#MSVC19.28 (VS16.9) default options (debug mode)
...
movzx eax, BYTE PTR [rax]
and eax, 3
cmp eax, 3
ja SHORT $LN2@bar
...
我正在查看以下 x86 汇编代码(Intel 语法):
movzx eax, al
and eax, 3
cmp eax, 3
ja loc_6BE9A0
据我了解,这应该等于 C:
中的类似内容eax &= 0xFF;
eax &= 3;
if (eax > 3)
loc_6BE9A0();
这似乎没有多大意义,因为这个条件永远不会为真(因为 eax
如果之前用 3 进行运算,则永远不会大于 3)。我是不是遗漏了什么,或者这真的只是一个不必要的条件?
而且:movzx eax, al
也不是必需的,如果它在那之后立即与 3 进行运算,对吗?
我问这个是因为我不太熟悉汇编语言,所以我不确定我是否遗漏了什么。
您是对的:鉴于以下 and
,movzx
是多余的。它可能是由非优化编译器生成的。
是的,如果此代码直接执行,则永远不会执行 ja
跳转。但是,如果其他地方有直接跳转到 cmp
(甚至 ja
)的代码,cmp/ja
可能不会完全没用。
这是多余的,不是您在优化的 asm 中看到的东西。
即使 cmp/ja
可能是来自其他地方的跳转目标,现有的优化编译器(如 GCC、clang、MSVC 和 ICC)也会(我很确定)执行 jmp
或不同的代码布局,而不是让执行落入始终为假的条件分支。优化器会知道这条执行路径不需要条件分支,因此会确保它没有遇到条件分支。 (即使这会额外花费 jmp
。)
这可能是一个不错的选择,即使在这种方式可以节省一些代码大小的假设情况下也是如此,因为您不想用不必要的条件分支污染/稀释分支预测历史,并且分支可以预测错误。
但是在调试模式下,一些编译器比其他编译器更能够关闭他们的大脑来优化单个语句或表达式。 (Across statements they'd always spill/reload vars to memory, 除非你使用 register int foo;
)
我能够欺骗 clang -O0
和 MSVC 发出准确的指令序列。还有类似的东西,但 GCC 更糟糕。
(令人惊讶的是,gcc -O0
仍然在单个表达式中进行一些优化,例如对 x /= 10;
使用乘法逆,并为 if(false)
删除死代码。与 MSVC 实际上将 0 放入寄存器和测试它是 0。)
void dummy();
unsigned char set_al();
int foo(void) {
if ((set_al() & 3) <= 3U)
dummy();
return 0;
}
clang12.0 for x86-64 Linux (on Godbolt)
push rbp
mov rbp, rsp
call set_al()
movzx eax, al # The redundant sequence
and eax, 3
cmp eax, 3
ja .LBB0_2
call dummy()
.LBB0_2:
xor eax, eax
pop rbp
ret
MSVC 包含相同的序列。 GCC10.3 类似但更糟,在寄存器中实现布尔值并 test
ing 它。 (两者也在同一个神箭link)
## GCC10.3
... set up RBP as a frame pointer
movzx eax, al # The redundant sequence
and eax, 3
cmp eax, 3
setbe al
test al, al # even worse than just jnbe
je .L2
call dummy()
.L2:
mov eax, 0
pop rbp
ret
由于 char
来自内存而不是 return 值,GCC 即使在调试模式下也确实优化了比较:
int bar(unsigned char *p) {
if ((*p & 3) <= 3U)
dummy();
return 0;
}
# GCC 10.3 -O0
bar(unsigned char*):
push rbp
mov rbp, rsp
sub rsp, 16 # space to spill the function arg
mov QWORD PTR [rbp-8], rdi
call dummy() # unconditional call
mov eax, 0
leave
ret
clang 和 MSVC 做测试,都用 asm like
#MSVC19.28 (VS16.9) default options (debug mode)
...
movzx eax, BYTE PTR [rax]
and eax, 3
cmp eax, 3
ja SHORT $LN2@bar
...