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"

是的。但是编译器会根据要求选择最快的方式。

  1. main - 编译器知道结果编译时间并且根本不调用函数 foo
  2. 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 标准描述了程序在抽象机中生成的内容,然后编译器可以生成与抽象机中的程序具有相同可观察行为的任何程序,即使它获得的结果完全不同比抽象机中的程序做的方式。