C11标准似乎暗示不应该优化具有常量控制表达式的迭代语句.我采纳了this answer的建议,它特别引用了标准草案中的6.8.5节:

其控制表达式不是常量表达式的迭代语句...可能会被执行机构假定为终止.

在这个答案中,它提到像while(1) ;这样的循环不应该进行优化.

所以为什么Clang/LLVM优化了下面的循环(用cc -O2 -std=c11 test.c -o test编译)?

#include <stdio.h>

static void die() {
    while(1)
        ;
}

int main() {
    printf("begin\n");
    die();
    printf("unreachable\n");
}

在我的机器上,这会打印出begin,然后是crashes on an illegal instruction(die()之后是ud2).On godbolt,我们可以看到在调用puts之后没有生成任何内容.

让Clang在-O2以下输出一个无限循环是一项非常困难的任务——虽然我可以反复测试volatile变量,但这涉及到我不想要的内存读取.如果我这样做:

#include <stdio.h>

static void die() {
    while(1)
        ;
}

int main() {
    printf("begin\n");
    volatile int x = 1;
    if(x)
        die();
    printf("unreachable\n");
}

...铿锵打印出begin,然后是unreachable,仿佛无限循环从未存在过.

如何让Clang在打开优化的情况下输出一个正确的无内存访问无限循环?

推荐答案

C11标准规定,6.8.5/6:

一种迭代语句,其控制表达式不是常量表达式,156)

这两个脚注并不规范,但提供了有用的信息:

156)省略的控制表达式替换为非零常量,这是一个常量表达式.

157)这是为了允许编译器转换,如删除空循环,即使在 无法证明终止合同.

在您的示例中,while(1)是一个清晰的常量表达式,因此它可能被实现假定为终止.这样的实现将无可救药地被 destruct ,因为"永远"循环是一种常见的编程构造.

然而,据我所知,循环之后"无法访问的代码"发生了什么,并没有明确定义.然而,当当确实表现得非常奇怪.将机器代码与GCC(X86)进行比较:

gcc 9.2 -O3 -std=c11 -pedantic-errors

.LC0:
        .string "begin"
main:
        sub     rsp, 8
        mov     edi, OFFSET FLAT:.LC0
        call    puts
.L2:
        jmp     .L2

叮当声9.0.0 -O3 -std=c11 -pedantic-errors

main:                                   # @main
        push    rax
        mov     edi, offset .Lstr
        call    puts
.Lstr:
        .asciz  "begin"

gcc生成循环,clang只是跑进树林,然后退出,错误为255.

我倾向于这种不顺从的叮当声行为.因为我试着把你的例子进一步扩展如下:

#include <stdio.h>
#include <setjmp.h>

static _Noreturn void die() {
    while(1)
        ;
}

int main(void) {
    jmp_buf buf;
    _Bool first = !setjmp(buf);

    printf("begin\n");
    if(first)
    {
      die();
      longjmp(buf, 1);
    }
    printf("unreachable\n");
}

我添加了C11_Noreturn,试图帮助编译器更进一步.应该清楚,仅从该关键字看,该函数将挂起.

setjmp在第一次执行时将返回0,因此该程序应该只会撞上while(1)并停在那里,只打印"BEGIN"(假设\n刷新stdout).这种情况发生在GCC身上.

如果只是删除了循环,它应该打印"开始"两次,然后打印"不可访问".但在clang(godbolt)上,它会打印"开始"1次,然后在返回退出代码0之前打印"不可访问".不管你怎么说,这都是大错特错.

我在这里找不到声称未定义行为的理由,所以我认为这是叮当声中的一个bug.无论如何,这种行为使得叮当声对嵌入式系统之类的程序毫无用处,在嵌入式系统中,你必须能够依赖永久循环来挂起程序(同时等待看门狗等).

C++相关问答推荐

在C中使用强制转换将uint16_t转换为uint8_t [2]是否有效?

为什么写入系统调用打印的字符数不正确?

无法用C++编译我的单元测试

为什么I2C会发送错误的数据?

如何跨平台处理UTF-16字符串?

struct 上的OpenMP缩减

文件权限为0666,但即使以超级用户身份也无法打开

如何识别Linux中USB集线器(根)和连接到集线器(根设备)的设备(子设备)?

防止C++中递归函数使用堆栈内存

C I/O:在Windows控制台上处理键盘输入

如何用C语言为CLI应用程序编写按键检测系统?

Caesar密码调试:输出文本末尾的问号和随机字符

*S=0;正在优化中.可能是GCC 13号虫?或者是一些不明确的行为?

我正在使用c学习数据 struct ,在学习堆栈时,我试图将中缀转换为后缀,并编写了这段代码.代码未给出输出

使用mmap为N整数分配内存

如何在C中计算包含递增和递减运算符的逻辑表达式?

模仿 memmove 的行为

使用共享变量同步多线程 C 中的函数

C语言程序流程解释

设置具有非零终止字符串的大整数