为什么(无限)递归在 clang 和 w/o -O3(和 gcc/g++)中给出不同的结果?

Why does (infinite) recursion give different results with and w/o -O3 in clang (and gcc/g++)?

当我编译 运行 这段代码时

#include <stdio.h>

int collatz(int a) {
    return a == 1 ? 1 : collatz((a%2) ? 3*a+1 : a/2);
}

int main() {
    for (int a = 0; a < 10; ++a)
        printf("%d: %d\n", a, collatz(a));
}

clang (10.0.0-4ubuntu1) 中它崩溃了,但是当我添加 -O3 时它 运行 没问题。 (当将其作为 C 或 C++ 代码传递给 clang 时,这两者都成立。)

我知道它在没有优化标志的情况下崩溃,因为用 0 调用 collatz 会导致无限递归。但是,-O3 是 returns 0: 1,这是错误的。这是优化中的(已知)错误还是我遗漏了什么?

(gcc/g++ (9.3.0) 也没有优化就崩溃,编译挂起 -O3。)

编辑:只是补充一点,我不是在寻找程序挂起/失败的原因(很明显),而是为什么编译器显然可以自由 return 一些值,而程序不应该 return (而是进入无限循环)。

编辑 2:为什么我没有选择一个更简单的例子?好吧,就是这样,这给出了相同的行为

int divide(int i) {
  return i == 1 ? 1 : divide(i/2);
} 

Is that a (known) bug in the optimization or am I missing something?

这不是优化中的错误。这是正在编译的程序中的错误。该程序的行为在 C++ 中未定义。

it returns 0: 1, which is wrong

当程序的行为未定义时,不存在“错误”行为。

but the compilation hangs with -O3

这可能是一个编译器错误。

how can the compiler just decide to return something, whereas, if you follow the program logic, it should not.

当程序的行为未定义时,编译器可以决定做任何事情或什么都不做。没有什么特别应该做或不应该做的。未定义的程序没有逻辑。

why the behaviour is undefined

C++ 标准说:

[intro.progress]

The implementation may assume that any thread will eventually do one of the following:

  • terminate,
  • make a call to a library I/O function,
  • perform an access through a volatile glvalue, or
  • perform a synchronization operation or an atomic operation.

如果参数为 0,线程将永远不会做任何这些事情,因此允许 [语言] 实现假定永远不会使用参数 0 调用该函数。当您调用具有此类参数的函数,您与此假设相矛盾,并且行为未定义。

注意:我的答案只在 C 中是正确的。正如在 C++ 中的其他答案中所说,程序中有 UB。这是您不应使用 C++ 编译器编译 C 代码的原因之一。

我试图通过godbolt获取clang 10.0.0生成的程序集。我知道不是每个人都知道如何阅读汇编,所以我会尽量简洁地解释我们可以从列表中推断出什么。这是我生成的:

clang -S main.c:

collatz(int):
        push    {r11, lr}
        mov     r11, sp
        sub     sp, sp, #16
        str     r0, [r11, #-4]
        ldr     r0, [r11, #-4]
        cmp     r0, #1
        bne     .LBB0_2
        b       .LBB0_1
.LBB0_1:
        mov     r0, #1
        str     r0, [sp, #8]            @ 4-byte Spill
        b       .LBB0_6
.LBB0_2:
        ldr     r0, [r11, #-4]
        add     r1, r0, r0, lsr #31
        bic     r1, r1, #1
        sub     r0, r0, r1
        cmp     r0, #0
        beq     .LBB0_4
        b       .LBB0_3
.LBB0_3:
        ldr     r0, [r11, #-4]
        add     r0, r0, r0, lsl #1
        add     r0, r0, #1
        str     r0, [sp, #4]            @ 4-byte Spill
        b       .LBB0_5
.LBB0_4:
        ldr     r0, [r11, #-4]
        add     r0, r0, r0, lsr #31
        asr     r0, r0, #1
        str     r0, [sp, #4]            @ 4-byte Spill
        b       .LBB0_5
.LBB0_5:
        ldr     r0, [sp, #4]            @ 4-byte Reload
        bl      collatz(int)
        str     r0, [sp, #8]            @ 4-byte Spill
        b       .LBB0_6
.LBB0_6:
        ldr     r0, [sp, #8]            @ 4-byte Reload
        mov     sp, r11
        pop     {r11, lr}
        bx      lr
main:
        push    {r11, lr}
        mov     r11, sp
        sub     sp, sp, #16
        mov     r0, #0
        str     r0, [r11, #-4]
        str     r0, [sp, #8]
        b       .LBB1_1
.LBB1_1:                                @ =>This Inner Loop Header: Depth=1
        ldr     r0, [sp, #8]
        cmp     r0, #9
        bgt     .LBB1_4
        b       .LBB1_2
.LBB1_2:                                @   in Loop: Header=BB1_1 Depth=1
        ldr     r0, [sp, #8]
        str     r0, [sp, #4]            @ 4-byte Spill
        bl      collatz(int)
        ldr     r1, .LCPI1_0
        str     r0, [sp]                @ 4-byte Spill
        mov     r0, r1
        ldr     r1, [sp, #4]            @ 4-byte Reload
        ldr     r2, [sp]                @ 4-byte Reload
        bl      printf
        b       .LBB1_3
.LBB1_3:                                @   in Loop: Header=BB1_1 Depth=1
        ldr     r0, [sp, #8]
        add     r0, r0, #1
        str     r0, [sp, #8]
        b       .LBB1_1
.LBB1_4:
        ldr     r0, [r11, #-4]
        mov     sp, r11
        pop     {r11, lr}
        bx      lr
.LCPI1_0:
        .long   .L.str
.L.str:
        .asciz  "%d: %d\n"

在这里,事情看起来很正常,函数已定义,它的行为看起来没问题,循环在 main 中正确形成,所以正如我们所料,当它调用 collat​​z(0) 时,堆栈溢出。

clang -S -O3 main.c:

collatz(int):
        mov     r0, #1
        bx      lr
main:
        push    {r4, r10, r11, lr}
        add     r11, sp, #8
        ldr     r4, .LCPI1_0
        mov     r1, #0
        mov     r2, #1
        mov     r0, r4
        bl      printf
        mov     r0, r4
        mov     r1, #1
        mov     r2, #1
        bl      printf
        mov     r0, r4
        mov     r1, #2
        mov     r2, #1
        bl      printf
        mov     r0, r4
        mov     r1, #3
        mov     r2, #1
        bl      printf
        mov     r0, r4
        mov     r1, #4
        mov     r2, #1
        bl      printf
        mov     r0, r4
        mov     r1, #5
        mov     r2, #1
        bl      printf
        mov     r0, r4
        mov     r1, #6
        mov     r2, #1
        bl      printf
        mov     r0, r4
        mov     r1, #7
        mov     r2, #1
        bl      printf
        mov     r0, r4
        mov     r1, #8
        mov     r2, #1
        bl      printf
        mov     r0, r4
        mov     r1, #9
        mov     r2, #1
        bl      printf
        mov     r0, #0
        pop     {r4, r10, r11, lr}
        bx      lr
.LCPI1_0:
        .long   .L.str
.L.str:
        .asciz  "%d: %d\n"

铿锵到这里彻底醉了。它确实像我们预期的那样优化了循环,但它出于某种原因决定 collat​​z(0) = 1 并且它编译了一个 very 奇怪的 collat​​z 版本,只是从不调用它。你可能会说 clang 是一个 C++ 编译器,所以它做意想不到的事情是有意义的,但事实并非如此,clang 将自己宣传为“Clang C、C++ 和 Objective-C 编译器”。对我来说,这是一个编译器错误。让我们尝试使用 clang 12.0.1 以防错误在更高版本中得到解决。

使用最新版本clang -S -O3 main.c:

    .text
    .file   "main.c"
    .globl  collatz                         # -- Begin function collatz
    .p2align    4, 0x90
    .type   collatz,@function
collatz:                                # @collatz
    .cfi_startproc
# %bb.0:
                                        # kill: def $edi killed $edi def $rdi
    cmpl    , %edi
    jne .LBB0_2
    jmp .LBB0_5
    .p2align    4, 0x90
.LBB0_3:                                #   in Loop: Header=BB0_2 Depth=1
    leal    (%rdi,%rdi,2), %edi
    addl    , %edi
    cmpl    , %edi
    je  .LBB0_5
.LBB0_2:                                # =>This Inner Loop Header: Depth=1
    testb   , %dil
    jne .LBB0_3
# %bb.4:                                #   in Loop: Header=BB0_2 Depth=1
    movl    %edi, %eax
    shrl    , %eax
    addl    %edi, %eax
    sarl    %eax
    movl    %eax, %edi
    cmpl    , %edi
    jne .LBB0_2
.LBB0_5:
    movl    , %eax
    retq
.Lfunc_end0:
    .size   collatz, .Lfunc_end0-collatz
    .cfi_endproc
                                        # -- End function
    .globl  main                            # -- Begin function main
    .p2align    4, 0x90
    .type   main,@function
main:                                   # @main
    .cfi_startproc
# %bb.0:
    .p2align    4, 0x90
.LBB1_1:                                # =>This Inner Loop Header: Depth=1
    jmp .LBB1_1
.Lfunc_end1:
    .size   main, .Lfunc_end1-main
    .cfi_endproc
                                        # -- End function
    .ident  "clang version 12.0.1"
    .section    ".note.GNU-stack","",@progbits
    .addrsig

这里,main 函数由一个 while(1) 循环组成,这是我们在给定输入时所期望的。

总而言之,我认为 gcc 和 clang 10.0.0 都有一个错误,尽管一个比另一个更容易被视为错误