C 中的短路评估未由编译器反映
The short circuit evaluation in C is not reflected by compiler
我正在尝试使用 GCC 和 Clang 的 -O3 优化标志编译以下 C 代码,在查看生成的汇编代码后,我发现这些编译器都没有实现 C standard 用于 && 运算符。
您可以参考下面的汇编代码了解更多信息,foo函数的前五行代码将按顺序运行,它会比较&&运算符的两个操作数,实际上违反了标准。那么,这里有什么误会吗?
C代码:
#include <stdio.h>
#include <stdbool.h>
void foo(int x, int y) {
bool logical = x && y;
printf("%d", logical);
}
int main(void) {
foo(1, 3);
return 0;
}
生成的汇编代码:
foo: # @foo
test edi, edi
setne al
test esi, esi
setne cl
and cl, al
movzx esi, cl
mov edi, offset .L.str
xor eax, eax
jmp printf # TAILCALL
main: # @main
push rax
mov edi, offset .L.str
mov esi, 1
xor eax, eax
call printf
xor eax, eax
pop rcx
ret
.L.str:
.asciz "%d"
是的。但是编译器会根据要求选择最快的方式。
main
- 编译器知道结果编译时间并且根本不调用函数 foo
foo
- 分支是非常昂贵的操作,因为它们需要刷新管道、潜在的缓存未命中、从慢速内存中读取等。因此在这种情况下分支没有意义。你的例子太简单了。
考虑一个不那么简单的例子:
bool bar(void);
void foo(int x) {
bool logical = x && bar();
printf("%d", logical);
}
int main(void) {
foo(1);
return 0;
}
foo:
test edi, edi
jne .L12
mov esi, edi
xor eax, eax
mov edi, OFFSET FLAT:.LC0
jmp printf
.L12:
sub rsp, 8
call bar
mov edi, OFFSET FLAT:.LC0
add rsp, 8
movzx esi, al
xor eax, eax
jmp printf
main:
sub rsp, 8
mov edi, 1
call foo
xor eax, eax
add rsp, 8
ret
来自 C 标准 6.5.13:
Unlike the bitwise binary & operator, the && operator guarantees
left-to-right evaluation; if the second operand is evaluated, there is
a sequence point between the evaluations of the first and second
operands. If the first operand compares equal to 0, the second operand
is not evaluated.
首先,你的例子有问题。
给定 x
= 1 和 y
= 3,评估 x && y
需要评估两个操作数,因为 &&
只有在两个操作数都为真时才为真。
其次,C 2018 5.1.2.3 6 说:
The least requirements on a conforming implementation are:
— Accesses to volatile objects are evaluated strictly according to the rules of the abstract machine.
— At program termination, all data written into files shall be identical to the result that execution of the program according to the abstract semantics would have produced.
— The input and output dynamics of interactive devices shall take place as specified in 7.21.3. The intent of these requirements is that unbuffered or line-buffered output appear as soon as possible, to ensure that prompting messages actually appear prior to a program waiting for input.
This is the observable behavior of the program.
这意味着编译器只负责确保上述行为按规定发生。如果可以从左操作数推导出结果,则不需要生成不评估 &&
或 ||
右操作数的汇编语言,只要该评估不改变可观察到的行为即可。编译器没有义务生成你寻求的汇编语言,只是为了确保可观察到的行为符合规定。
当标准描述表达式是否被求值时,它是在“抽象机器”(C 2018 5.1.2.3 1) 的上下文中描述它们,而不是描述生成的程序必须在程序集上实际做什么语言水平。这个想法是 C 标准描述了程序在抽象机中生成的内容,然后编译器可以生成与抽象机中的程序具有相同可观察行为的任何程序,即使它获得的结果完全不同比抽象机中的程序做的方式。
我正在尝试使用 GCC 和 Clang 的 -O3 优化标志编译以下 C 代码,在查看生成的汇编代码后,我发现这些编译器都没有实现 C standard 用于 && 运算符。
您可以参考下面的汇编代码了解更多信息,foo函数的前五行代码将按顺序运行,它会比较&&运算符的两个操作数,实际上违反了标准。那么,这里有什么误会吗?
C代码:
#include <stdio.h>
#include <stdbool.h>
void foo(int x, int y) {
bool logical = x && y;
printf("%d", logical);
}
int main(void) {
foo(1, 3);
return 0;
}
生成的汇编代码:
foo: # @foo
test edi, edi
setne al
test esi, esi
setne cl
and cl, al
movzx esi, cl
mov edi, offset .L.str
xor eax, eax
jmp printf # TAILCALL
main: # @main
push rax
mov edi, offset .L.str
mov esi, 1
xor eax, eax
call printf
xor eax, eax
pop rcx
ret
.L.str:
.asciz "%d"
是的。但是编译器会根据要求选择最快的方式。
main
- 编译器知道结果编译时间并且根本不调用函数foo
foo
- 分支是非常昂贵的操作,因为它们需要刷新管道、潜在的缓存未命中、从慢速内存中读取等。因此在这种情况下分支没有意义。你的例子太简单了。
考虑一个不那么简单的例子:
bool bar(void);
void foo(int x) {
bool logical = x && bar();
printf("%d", logical);
}
int main(void) {
foo(1);
return 0;
}
foo:
test edi, edi
jne .L12
mov esi, edi
xor eax, eax
mov edi, OFFSET FLAT:.LC0
jmp printf
.L12:
sub rsp, 8
call bar
mov edi, OFFSET FLAT:.LC0
add rsp, 8
movzx esi, al
xor eax, eax
jmp printf
main:
sub rsp, 8
mov edi, 1
call foo
xor eax, eax
add rsp, 8
ret
来自 C 标准 6.5.13:
Unlike the bitwise binary & operator, the && operator guarantees left-to-right evaluation; if the second operand is evaluated, there is a sequence point between the evaluations of the first and second operands. If the first operand compares equal to 0, the second operand is not evaluated.
首先,你的例子有问题。
给定 x
= 1 和 y
= 3,评估 x && y
需要评估两个操作数,因为 &&
只有在两个操作数都为真时才为真。
其次,C 2018 5.1.2.3 6 说:
The least requirements on a conforming implementation are:
— Accesses to volatile objects are evaluated strictly according to the rules of the abstract machine.
— At program termination, all data written into files shall be identical to the result that execution of the program according to the abstract semantics would have produced.
— The input and output dynamics of interactive devices shall take place as specified in 7.21.3. The intent of these requirements is that unbuffered or line-buffered output appear as soon as possible, to ensure that prompting messages actually appear prior to a program waiting for input.
This is the observable behavior of the program.
这意味着编译器只负责确保上述行为按规定发生。如果可以从左操作数推导出结果,则不需要生成不评估 &&
或 ||
右操作数的汇编语言,只要该评估不改变可观察到的行为即可。编译器没有义务生成你寻求的汇编语言,只是为了确保可观察到的行为符合规定。
当标准描述表达式是否被求值时,它是在“抽象机器”(C 2018 5.1.2.3 1) 的上下文中描述它们,而不是描述生成的程序必须在程序集上实际做什么语言水平。这个想法是 C 标准描述了程序在抽象机中生成的内容,然后编译器可以生成与抽象机中的程序具有相同可观察行为的任何程序,即使它获得的结果完全不同比抽象机中的程序做的方式。