nne*_*neo 141 c clang compiler-optimization language-lawyer
C11 标准似乎暗示不应优化带有常量控制表达式的迭代语句。我从这个答案中得到了我的建议,它特别引用了标准草案中的第 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");
}
Run Code Online (Sandbox Code Playgroud)
在我的机器上,这会打印出begin,然后在非法指令(ud2放置在 之后的陷阱die())上崩溃。在 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");
}
Run Code Online (Sandbox Code Playgroud)
... Clang 打印begin后跟unreachable好像无限循环从未存在过一样。
你如何让 Clang 在优化打开的情况下输出一个正确的、无内存访问的无限循环?
Lun*_*din 83
C11 标准是这样说的,6.8.5/6:
控制表达式不是常量表达式的迭代语句,156)不执行输入/输出操作,不访问易失性对象,并且在其主体中不执行同步或原子操作,控制表达式,或(在 for语句)它的表达式 3,可以被实现假设终止。157)
这两个脚注不是规范性的,但提供了有用的信息:
156) 一个省略的控制表达式被一个非零常量替换,这是一个常量表达式。
157)这是为了允许编译器转换,例如即使在无法证明终止时也可以删除空循环。
在您的情况下,while(1)是一个非常清晰的常量表达式,因此实现可能不会假设它终止。这样的实现将无可救药地被破坏,因为“永远”循环是一种常见的编程结构。
然而,据我所知,循环后“无法访问的代码”会发生什么,并没有明确定义。但是,clang 的行为确实很奇怪。将机器代码与 gcc (x86) 进行比较:
海湾合作委员会 9.2 -O3 -std=c11 -pedantic-errors
.LC0:
.string "begin"
main:
sub rsp, 8
mov edi, OFFSET FLAT:.LC0
call puts
.L2:
jmp .L2
Run Code Online (Sandbox Code Playgroud)
叮当 9.0.0 -O3 -std=c11 -pedantic-errors
main: # @main
push rax
mov edi, offset .Lstr
call puts
.Lstr:
.asciz "begin"
Run Code Online (Sandbox Code Playgroud)
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");
}
Run Code Online (Sandbox Code Playgroud)
我添加了 C11_Noreturn试图帮助编译器进一步发展。应该很清楚,这个函数将挂断,仅从那个关键字。
setjmp将在第一次执行时返回 0,所以这个程序应该只是撞到while(1)那里并停在那里,只打印“开始”(假设 \n 刷新标准输出)。这发生在 gcc 中。
如果循环被简单地删除,它应该打印“begin”2 次,然后打印“unreachable”。然而,在clang(godbolt)上,它会在返回退出代码0之前打印“begin”1次然后“unreachable”。无论你怎么写,这都是完全错误的。
我在这里找不到任何声明未定义行为的案例,所以我认为这是 clang 中的一个错误。无论如何,这种行为使 clang 100% 对嵌入式系统等程序毫无用处,在这些程序中,您必须能够依靠挂起程序的永恒循环(在等待看门狗等时)。
P__*_*J__ 60
您需要插入一个可能导致副作用的表达式。
最简单的解决方案:
static void die() {
while(1)
__asm("");
}
Run Code Online (Sandbox Code Playgroud)
Arn*_*ion 58
其他答案已经涵盖了使用内联汇编语言或其他副作用使 Clang 发出无限循环的方法。我只是想确认这确实是一个编译器错误。具体来说,这是一个长期存在的 LLVM 错误——它将 C++ 概念“没有副作用的所有循环必须终止”应用于不应该终止的语言,例如 C。该错误最终在 LLVM 12 中得到修复。
例如,Rust 编程语言也允许无限循环并使用 LLVM 作为后端,它也有同样的问题。
LLVM 12 添加了一个mustprogress前端可以省略的属性,以指示函数何时不一定返回,并且更新了 clang 12 以解决这个问题。您可以看到您的示例使用 clang 12.0.0正确编译,而使用 clang 11.0.1则没有
Pet*_*des 34
... 内联包含无限循环的函数时。当while(1);直接出现在 main 中时,行为是不同的,这对我来说很臭。
有关摘要和链接,请参阅@Arnavion 的回答。这个答案的其余部分是在我确认这是一个错误之前写的,更不用说一个已知的错误了。
回答标题问题:如何制作一个不会被优化掉的无限空循环?? -在 Clang 3.9 及更高版本中
创建die()一个宏,而不是一个函数来解决这个错误。(早期的 Clang 版本要么保持循环,要么call使用无限循环向函数的非内联版本发出 a。)即使print;while(1);print;函数内联到其调用者(Godbolt)中,这似乎也是安全的。 -std=gnu11vs.-std=gnu99不会改变任何东西。
如果您只关心 GNU C,循环内的P__J____asm__("");也可以工作,并且不应该损害任何理解它的编译器对任何周围代码的优化。GNU C Basic asm 语句是隐式的volatile,因此这被视为一个可见的副作用,它必须像在 C 抽象机器中那样多次“执行”。(是的,Clang 实现了 C 的 GNU 方言,如 GCC 手册所述。)
有些人认为优化掉一个空的无限循环可能是合法的。我不同意1,但即使我们接受这一点,就不能也成为合法的Clang的承担循环之后的语句是不可达,并让执行脱落功能的结束到下一个功能,或者进入垃圾解码为随机指令。
(这将符合 Clang++ 的标准(但仍然不是很有用);没有任何副作用的无限循环是 C++ 中的 UB,但不是 C。
是 while(1);C 中的未定义行为? UB 使编译器基本上可以发出任何内容对于肯定会遇到 UB 的执行路径上的代码。asm循环中的语句将避免 C++ 的 UB。但实际上,Clang 编译为 C++ 不会删除常量表达式无限空循环,除非内联时,与 when 相同编译为 C。)
手动内联会while(1);改变 Clang 的编译方式:asm 中存在无限循环。 这是我们对规则律师 POV 的期望。
#include <stdio.h>
int main() {
printf("begin\n");
while(1);
//infloop_nonconst(1);
//infloop();
printf("unreachable\n");
}
Run Code Online (Sandbox Code Playgroud)
在 Godbolt 编译器资源管理器上,Clang 9.0 -O3 编译为 C ( -xc) for x86-64:
main: # @main
push rax # re-align the stack by 16
mov edi, offset .Lstr # non-PIE executable can use 32-bit absolute addresses
call puts
.LBB3_1: # =>This Inner Loop Header: Depth=1
jmp .LBB3_1 # infinite loop
.section .rodata
...
.Lstr:
.asciz "begin"
Run Code Online (Sandbox Code Playgroud)
具有相同选项的同一个编译器编译 amain调用infloop() { while(1); }相同的 first puts,但在那之后停止发出指令main。因此,正如我所说,执行只是从函数的末尾开始,进入下一个函数(但堆栈未对齐以用于函数入口,因此它甚至不是有效的尾调用)。
有效的选择是
label: jmp label无限循环return 0从main.崩溃或以其他方式继续而不打印“无法访问”显然不适用于 C11 实现,除非有我没有注意到的 UB。
脚注 1:
作为记录,我同意@Lundin 的回答,该回答引用了证据标准,即 C11 不允许假设常量表达式无限循环终止,即使它们为空(无 I/O、易失性、同步或其他可见的副作用)。
这是一组条件,可以将循环编译为普通 CPU的空 asm 循环。(即使源中的主体不是空的,在循环运行时,没有数据竞争 UB 的其他线程或信号处理程序也无法看到对变量的赋值。因此,如果需要,符合要求的实现可以删除此类循环主体到。然后就留下了是否可以删除循环本身的问题。ISO C11 明确说不。)
鉴于 C11 将这种情况作为一种实现不能假设循环终止(并且它不是 UB)的情况,很明显他们打算在运行时出现循环。以 CPU 为目标的实现,其执行模型不能在有限时间内完成无限量的工作,没有理由删除空的常量无限循环。甚至一般来说,确切的措辞是关于它们是否可以“假设终止”。如果循环无法终止,则意味着无法访问后面的代码,无论您对数学和无穷大提出什么论点,以及在某个假设的机器上完成无限量的工作需要多长时间。
此外,Clang 不仅仅是符合 ISO C 的 DeathStation 9000,它还旨在用于现实世界的低级系统编程,包括内核和嵌入式内容。 因此,无论您是否接受有关 C11允许删除 的论点while(1);,Clang 想要真正这样做是没有意义的。如果你写while(1);,那可能不是意外。删除意外结束的循环(使用运行时变量控制表达式)可能很有用,编译器这样做是有意义的。
您很少想一直旋转直到下一个中断,但是如果您用 C 编写它,那绝对是您期望发生的事情。(以及在 GCC 和 Clang中会发生什么,当无限循环在包装函数内时 Clang 除外)。
例如,在原始 OS 内核中,当调度程序没有要运行的任务时,它可能会运行空闲任务。第一个实现可能是while(1);.
或者对于没有任何省电空闲功能的硬件,这可能是唯一的实现。(直到 2000 年代初,我认为这在 x86 上并不少见。虽然该hlt指令确实存在,但 IDK 如果在 CPU 开始处于低功耗空闲状态之前节省了大量电量。)
jon*_*njo 15
只是为了记录,Clang 也行为不端goto:
static void die() {
nasty:
goto nasty;
}
int main() {
int x; printf("begin\n");
die();
printf("unreachable\n");
}
Run Code Online (Sandbox Code Playgroud)
它产生与问题相同的输出,即:
main: # @main
push rax
mov edi, offset .Lstr
call puts
.Lstr:
.asciz "begin"
Run Code Online (Sandbox Code Playgroud)
我看不到 C11 中允许的任何方式来阅读它,它只说:
6.8.6.1(2)
goto语句导致无条件跳转到封闭函数中以命名标签为前缀的语句。
由于goto不是“迭代声明”(6.8.5 列表while,do和for),没有任何关于特殊“假设终止”的放纵适用,但是您想阅读它们。
每个原始问题的 Godbolt 链接编译器是 x86-64 Clang 9.0.0,标志是 -g -o output.s -mllvm --x86-asm-syntax=intel -S --gcc-toolchain=/opt/compiler-explorer/gcc-9.2.0 -fcolor-diagnostics -fno-crash-diagnostics -O2 -std=c11 example.c
使用 x86-64 GCC 9.2 等其他版本,您将获得非常完美的效果:
.LC0:
.string "begin"
main:
sub rsp, 8
mov edi, OFFSET FLAT:.LC0
call puts
.L2:
jmp .L2
Run Code Online (Sandbox Code Playgroud)
标志: -g -o output.s -masm=intel -S -fdiagnostics-color=always -O2 -std=c11 example.c
我将扮演魔鬼的拥护者并争辩说标准没有明确禁止编译器优化无限循环。
控制表达式不是常量表达式的迭代语句,156)不执行输入/输出操作,不访问易失性对象,并且在其主体中不执行同步或原子操作,控制表达式,或(在 for语句)它的表达式 3,可以被实现假定为终止。157)
我们来解析一下。可以假设满足某些标准的迭代语句终止:
if (satisfiesCriteriaForTerminatingEh(a_loop))
if (whatever_reason_or_just_because_you_feel_like_it)
assumeTerminates(a_loop);
Run Code Online (Sandbox Code Playgroud)
这并没有说明如果不满足标准会发生什么,并且假设循环可能会终止,只要遵守标准的其他规则,即使这样也不会明确禁止。
do { } while(0)或者while(0){}毕竟是不满足条件的迭代语句(循环),这些条件允许编译器随心所欲地假设它们终止,但它们显然确实终止了。
但是编译器可以优化while(1){}吗?
在抽象机中,所有表达式都按照语义指定的方式进行评估。如果一个实际的实现可以推断出它的值未被使用并且没有产生所需的副作用(包括由调用函数或访问易失性对象引起的任何副作用),则它不需要计算表达式的一部分。
这提到了表达式,而不是语句,所以它不是 100% 令人信服的,但它肯定允许这样的调用:
void loop(void){ loop(); }
int main()
{
loop();
}
Run Code Online (Sandbox Code Playgroud)
被跳过。有趣的是,clang 确实跳过了它,而 gcc 没有。
| 归档时间: |
|
| 查看次数: |
29070 次 |
| 最近记录: |