如何制作一个不会被优化掉的无限空循环?
How do I make an infinite empty loop that won't be optimized away?
C11 标准似乎暗示不应优化带有常量控制表达式的迭代语句。我的建议来自 this answer,它特别引用了标准草案的第 6.8.5 节:
An iteration statement whose controlling expression is not a constant expression ... may be assumed by the implementation to terminate.
在那个答案中提到像 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
,然后 在非法指令上崩溃 (在 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");
}
...Clang 打印 begin
后跟 unreachable
就好像无限循环不存在一样。
如何让 Clang 输出正确的、无内存访问的无限循环并启用优化?
我确信这只是一个普通的老错误。我把我的测试留在下面,特别是参考标准委员会的讨论,因为我以前有过一些推理。
我认为这是未定义的行为(见末),Clang 只有一种实现。 GCC 确实按您预期的那样工作,只优化了 unreachable
打印语句但离开了循环。一些 Clang 在组合内联和确定它可以用循环做什么时如何做出奇怪的决定。
该行为非常奇怪 - 它删除了最终打印,因此 "seeing" 无限循环,但随后也摆脱了循环。
据我所知,情况更糟。删除内联我们得到:
die: # @die
.LBB0_1: # =>This Inner Loop Header: Depth=1
jmp .LBB0_1
main: # @main
push rax
mov edi, offset .Lstr
call puts
.Lstr:
.asciz "begin"
所以函数被创建,调用被优化。这比预期的更有弹性:
#include <stdio.h>
void die(int x) {
while(x);
}
int main() {
printf("begin\n");
die(1);
printf("unreachable\n");
}
导致函数的汇编非常不理想,但函数调用再次被优化!更糟的是:
void die(x) {
while(x++);
}
int main() {
printf("begin\n");
die(1);
printf("unreachable\n");
}
我通过添加局部变量并增加它、传递指针、使用 goto
等进行了一系列其他测试...在这一点上我会放弃。如果一定要用clang
static void die() {
int volatile x = 1;
while(x);
}
完成任务。它在优化方面很糟糕(很明显),并留下了多余的最终 printf
。至少程序不会停止。也许 GCC 毕竟?
附录
在与 David 讨论后,我认为标准没有说明 "if the condition is constant, you may not assume the loop terminates"。因此,并且根据标准授予没有可观察的行为(如标准中定义的),我只会争论一致性 - 如果编译器正在优化一个循环,因为它假设它终止,它不应该优化以下语句。
哎呀n1528 如果我没看错的话,这些都是未定义的行为。具体
A major issue for doing so is that it allows code to move across a potentially non-terminating loop
从这里我认为它只能转变成对我们想要(预期的?)而不是允许的讨论。
循环没有副作用,所以可以优化掉。该循环实际上是零工作单元的无限次迭代。这在数学和逻辑中是未定义的,并且标准没有说明如果每件事都可以在零时间内完成,是否允许实现完成无限多的事情。 Clang 的解释将无穷大乘以零视为零而不是无穷大是完全合理的。该标准没有说明如果循环中的所有工作实际上都已完成,无限循环是否可以结束。
允许编译器优化掉标准中定义的任何不可观察的行为。这包括执行时间。不需要保留这样一个事实,即循环如果不进行优化,将花费无限长的时间。允许将其更改为更短的 运行 时间——事实上,这是大多数优化的重点。您的循环已优化。
即使 clang 天真地翻译了代码,您也可以想象一个优化 CPU 可以在前一次迭代所用时间的一半时间内完成每次迭代。这实际上会在有限的时间内完成无限循环。这样的优化CPU是否违反标准?如果说优化 CPU 太擅长优化就会违反标准,这似乎很荒谬。编译器也是如此。
您需要插入一个可能导致副作用的表达式。
最简单的解决方案:
static void die() {
while(1)
__asm("");
}
C11 标准是这样说的,6.8.5/6:
An iteration statement whose controlling expression is not a constant expression,156) that
performs no input/output operations, does not access volatile objects, and performs no
synchronization or atomic operations in its body, controlling expression, or (in the case of a for statement) its expression-3, may be assumed by the implementation to
terminate.157)
两个脚注不是规范性的,但提供了有用的信息:
156) An omitted controlling expression is replaced by a nonzero constant, which is a constant expression.
157) This is intended to allow compiler transformations such as removal of empty loops even when
termination cannot be proven.
在您的例子中,while(1)
是一个 crystal 明确的常量表达式,因此它可能 不会 被实现假设为终止。这样的实现将无可救药地被破坏,因为 "for-ever" 循环是一种常见的编程结构。
然而,据我所知,循环后 "unreachable code" 会发生什么,并没有明确定义。然而,clang 确实表现得很奇怪。机器代码与 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
clang 9.0.0 -O3 -std=c11 -pedantic-errors
main: # @main
push rax
mov edi, offset .Lstr
call puts
.Lstr:
.asciz "begin"
gcc 生成循环,clang 只是跑到树林里,并以错误 255 退出。
我倾向于这是 clang 的不合规行为。因为我试图像这样进一步扩展您的示例:
#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
将在第一次执行时 return 0,所以这个程序应该直接进入 while(1)
并停在那里,只打印 "begin" (假设 \n 刷新标准输出)。 gcc 会发生这种情况。
如果简单地删除循环,它应该打印 "begin" 2 次然后打印 "unreachable"。然而,在 clang 上(godbolt),它打印 "begin" 1 次,然后在 returning 退出代码 0 之前打印 "unreachable"。无论你怎么说,这都是完全错误的。
我在这里找不到声明未定义行为的案例,所以我认为这是 clang 中的一个错误。无论如何,这种行为使 clang 对嵌入式系统等程序 100% 无用,在嵌入式系统中,您必须能够依赖挂起程序的永恒循环(在等待看门狗等时)。
我会唱反调并争辩说该标准并未明确禁止编译器优化无限循环。
An iteration statement whose controlling expression is not a constant
expression,156) that performs no input/output operations, does not
access volatile objects, and performs no synchronization or atomic
operations in its body, controlling expression, or (in the case of a
for statement) its expression-3, may be assumed by the implementation
to terminate.157)
我们来分析一下。满足特定条件的迭代语句可以假定终止:
if (satisfiesCriteriaForTerminatingEh(a_loop))
if (whatever_reason_or_just_because_you_feel_like_it)
assumeTerminates(a_loop);
这并没有说明如果不满足标准会发生什么,并且只要遵守标准的其他规则,就不会明确禁止循环可能终止的假设。
do { } while(0)
或 while(0){}
毕竟是不满足允许编译器随心所欲地假设它们终止但它们显然确实终止的标准的所有迭代语句(循环) .
但是编译器可以优化while(1){}
出来吗?
5.1.2.3p4 说:
In the abstract machine, all expressions are evaluated as specified by
the semantics. An actual implementation need not evaluate part of an
expression if it can deduce that its value is not used and that no
needed side effects are produced (including any caused by calling a
function or accessing a volatile object).
这里提到的是表达式,而不是语句,所以它不是 100% 令人信服,但它肯定允许这样的调用:
void loop(void){ loop(); }
int main()
{
loop();
}
被跳过。有趣的是,clang does skip it, and gcc doesn't.
这是一个 Clang 错误
... 内联包含无限循环的函数时。当 while(1);
直接出现在 main 中时,行为是不同的,这对我来说很臭。
有关摘要和链接,请参阅 @Arnavion's answer。这个答案的其余部分是在我确认这是一个错误之前写的,更不用说一个已知的错误了。
回答标题问题:如何制作一个不会被优化掉的无限空循环?? -
将 die()
设为宏,而不是函数 ,以解决 Clang 3.9 及更高版本中的此错误。 (早期的 Clang 版本 keeps the loop or emits a call
to a non-inline version of the function with the infinite loop.) That appears to be safe even if the print;while(1);print;
function inlines into its caller (Godbolt)。 -std=gnu11
与 -std=gnu99
没有任何改变。
如果您只关心 GNU C,P__J__'s __asm__("");
inside the loop also works, and shouldn't hurt optimization of any surrounding code for any compilers that understand it. GNU C Basic asm statements are implicitly volatile
,那么这算作一个可见的 side-effect,它必须 "execute" 与在 C 抽象机中一样多。 (是的,Clang 实现了 C 的 GNU 方言,如 GCC 手册所述。)
有些人认为优化空的无限循环可能是合法的。我不同意1,但即使我们接受这一点,也不能也 Clang 假设是合法的循环后的语句无法访问, 并让执行从函数末尾进入下一个函数,或进入解码为随机指令的垃圾。
(对于 Clang++ 来说是 standards-compliant(但仍然不是很有用);没有任何副作用的无限循环是 C++ 中的 UB,但不是 C。
Is while(1); undefined behavior in C? UB 让编译器基本上为执行路径上肯定会遇到 UB 的代码发出任何内容。循环中的 asm
语句将避免此 UB for C++。但实际上,Clang 编译为 C++ 不会删除 constant-expression 无限空循环,除非内联,与编译为 C 时相同。)
手动内联 while(1);
改变了 Clang 编译它的方式:asm 中存在无限循环。 这是我们对 rules-lawyer POV 的期望。
#include <stdio.h>
int main() {
printf("begin\n");
while(1);
//infloop_nonconst(1);
//infloop();
printf("unreachable\n");
}
On the Godbolt compiler explorer,Clang 9.0 -O3 编译为适用于 x86-64 的 C (-xc
):
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"
具有相同选项的相同编译器编译一个 main
,它首先调用 infloop() { while(1); }
到相同的 puts
,但之后就停止为 main
发出指令观点。因此,正如我所说,执行只是从函数的末尾开始,进入下一个函数(但堆栈未对齐函数入口,因此它甚至不是有效的尾调用)。
有效选项是
- 发出一个
label: jmp label
无限循环
- 或者(如果我们接受无限循环可以被移除)发出另一个调用来打印第二个字符串,然后
return 0
从 main
.
崩溃或以其他方式继续而不打印 "unreachable" 显然对于 C11 实现来说是不行的,除非有我没有注意到的 UB。
脚注 1:
郑重声明,我同意 @Lundin's answer which cites the standard 的证据,即 C11 不允许假设 constant-expression 无限循环终止,即使它们是空的(没有 I/O 、volatile、同步或其他可见的 side-effects).
这是一组条件,可以将循环编译为空 asm 循环 用于正常 CPU。 (即使 body 在源代码中不为空,在循环为 运行ning 时,没有 data-race UB 的情况下,对变量的赋值对其他线程或信号处理程序是不可见的。所以如果需要,符合规范的实现可以删除此类循环体。然后就剩下循环本身是否可以删除的问题。ISO C11 明确表示不可以。)
鉴于 C11 将这种情况作为实现不能假设循环终止的情况(并且它不是 UB),很明显他们打算在 run-time 处出现循环。一个以 CPUs 为目标的实现,其执行模型不能在有限时间内完成无限量的工作,没有理由删除一个空的常量无限循环。或者甚至在一般情况下,确切的措辞是关于它们是否可以 "assumed to terminate" 。如果循环无法终止,则意味着后面的代码无法访问,无论 what arguments you make 关于数学和无穷大以及在某个假设的机器上做无限量的工作需要多长时间。
此外,Clang 不仅仅是一个符合 ISO C 的 DeathStation 9000,它旨在用于 real-world low-level 系统编程,包括内核和嵌入式东西. 因此,无论您是否接受关于 C11 允许 删除 while(1);
的论点,Clang 想要实际这样做是没有意义的。如果你写 while(1);
,那可能不是偶然的。删除意外无限结束的循环(使用 运行 时间变量控制表达式)可能很有用,编译器这样做很有意义。
您很少想一直循环到下一个中断,但是如果您用 C 语言编写它,那绝对是您期望发生的事情。 (还有 doe 发生在 GCC 和 Clang 中,但当无限循环在包装函数内时 Clang 除外。
例如,在原始 OS 内核中,当调度程序没有要 运行 的任务时,它可能 运行 空闲任务。第一个实现可能是 while(1);
.
或者对于没有任何 power-saving 空闲功能的硬件,这可能是唯一的实现。 (直到 2000 年代初期,我认为这在 x86 上并不少见。尽管 hlt
指令确实存在,但 IDK 如果它节省了有意义的电量,直到 CPUs 开始有 low-power 空闲州。)
以下似乎对我有用:
#include <stdio.h>
__attribute__ ((optnone))
static void die(void) {
while (1) ;
}
int main(void) {
printf("begin\n");
die();
printf("unreachable\n");
}
在 godbolt
明确告诉 Clang 不要优化一个函数会导致按预期发出无限循环。希望有一种方法可以选择性地禁用特定的优化,而不是像那样将它们全部关闭。不过,Clang 仍然拒绝为第二个 printf
发出代码。为了强制它这样做,我不得不进一步将 main
中的代码修改为:
volatile int x = 0;
if (x == 0)
die();
您似乎需要禁用无限循环函数的优化,然后确保有条件地调用无限循环。在现实世界中,反正后者几乎总是如此。
其他答案已经涵盖了使用内联汇编语言或其他副作用使 Clang 发出无限循环的方法。我只是想确认这确实是一个编译器错误。具体来说,它是 a long-standing LLVM bug - 它将“所有没有副作用的循环都必须终止”的 C++ 概念应用于不应终止的语言,例如 C。该错误最终在 LLVM 12 中得到修复。
例如,the Rust programming language also allows infinite loops and uses LLVM as a backend, and it had this same issue.
LLVM 12 添加了一个 mustprogress
属性,前端可以忽略该属性以指示何时函数不一定 return,并且更新了 clang 12 以说明它。您可以看到您的示例编译正确 with clang 12.0.0 whereas it did not with clang 11.0.1
郑重声明,Clang 也对 goto
:
行为不当
static void die() {
nasty:
goto nasty;
}
int main() {
int x; printf("begin\n");
die();
printf("unreachable\n");
}
它产生与问题中相同的输出,即:
main: # @main
push rax
mov edi, offset .Lstr
call puts
.Lstr:
.asciz "begin"
我看不出任何 C11 允许的方式来阅读它,它只说:
6.8.6.1(2) A goto
statement causes an unconditional jump to the statement prefixed by the named label in the enclosing function.
因为 goto
不是 "iteration statement"(6.8.5 列出了 while
、do
和 for
)与特殊的 [=37= 无关] 放纵适用,但你想阅读它们。
每个原始问题的 Godbolt link 编译器是 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
标志:-g -o output.s -masm=intel -S -fdiagnostics-color=always -O2 -std=c11 example.c
一个符合规范的实现可能,而且许多实际的实现,对一个程序可以执行多长时间或者它可以执行多少条指令强加任意限制,并且如果这些限制被违反或者 - 在 "as-if" rule--如果它决定了他们不可避免地会被违反。如果一个实现可以成功地处理至少一个名义上执行 N1570 5.2.4.1 中列出的所有限制而没有达到任何翻译限制的程序,则限制的存在、记录的范围以及超过限制的影响是标准管辖范围之外的所有实施质量问题。
我认为该标准的意图非常明确,即编译器不应假定没有副作用的 while(1) {}
循环或 break
语句会终止。与某些人可能认为的相反,该标准的作者并没有邀请编译器编写者变得愚蠢或迟钝。一个符合规范的实现可能有助于决定终止任何程序,如果不被中断,该程序将执行比宇宙中的原子更多的无副作用指令,但高质量的实现不应该基于任何假设执行这样的操作终止,而是基于这样做可能有用,并且不会(与 clang 的行为不同)比无用更糟糕。
看来这是Clang编译器的一个bug。如果 die()
函数没有任何强制成为静态函数,则取消 static
并使其成为 inline
:
#include <stdio.h>
inline void die(void) {
while(1)
;
}
int main(void) {
printf("begin\n");
die();
printf("unreachable\n");
}
它在使用 Clang 编译器编译时按预期工作,并且也是可移植的。
Compiler Explorer (godbolt.org) - clang 9.0.0 -O3 -std=c11 -pedantic-errors
main: # @main
push rax
mov edi, offset .Lstr
call puts
.LBB0_1: # =>This Inner Loop Header: Depth=1
jmp .LBB0_1
.Lstr:
.asciz "begin"
很抱歉,如果情况并非如此荒谬,我偶然发现了这个 post 并且我知道因为我多年来使用 Gentoo Linux 发行版,如果您希望编译器不优化您的你应该使用-O0(零)的代码。我对此很好奇,并编译并 运行 上面的代码,并且循环无限期地进行下去。使用 clang-9 编译:
cc -O0 -std=c11 test.c -o test
C11 标准似乎暗示不应优化带有常量控制表达式的迭代语句。我的建议来自 this answer,它特别引用了标准草案的第 6.8.5 节:
An iteration statement whose controlling expression is not a constant expression ... may be assumed by the implementation to terminate.
在那个答案中提到像 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
,然后 在非法指令上崩溃 (在 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");
}
...Clang 打印 begin
后跟 unreachable
就好像无限循环不存在一样。
如何让 Clang 输出正确的、无内存访问的无限循环并启用优化?
我确信这只是一个普通的老错误。我把我的测试留在下面,特别是参考标准委员会的讨论,因为我以前有过一些推理。
我认为这是未定义的行为(见末),Clang 只有一种实现。 GCC 确实按您预期的那样工作,只优化了 unreachable
打印语句但离开了循环。一些 Clang 在组合内联和确定它可以用循环做什么时如何做出奇怪的决定。
该行为非常奇怪 - 它删除了最终打印,因此 "seeing" 无限循环,但随后也摆脱了循环。
据我所知,情况更糟。删除内联我们得到:
die: # @die
.LBB0_1: # =>This Inner Loop Header: Depth=1
jmp .LBB0_1
main: # @main
push rax
mov edi, offset .Lstr
call puts
.Lstr:
.asciz "begin"
所以函数被创建,调用被优化。这比预期的更有弹性:
#include <stdio.h>
void die(int x) {
while(x);
}
int main() {
printf("begin\n");
die(1);
printf("unreachable\n");
}
导致函数的汇编非常不理想,但函数调用再次被优化!更糟的是:
void die(x) {
while(x++);
}
int main() {
printf("begin\n");
die(1);
printf("unreachable\n");
}
我通过添加局部变量并增加它、传递指针、使用 goto
等进行了一系列其他测试...在这一点上我会放弃。如果一定要用clang
static void die() {
int volatile x = 1;
while(x);
}
完成任务。它在优化方面很糟糕(很明显),并留下了多余的最终 printf
。至少程序不会停止。也许 GCC 毕竟?
附录
在与 David 讨论后,我认为标准没有说明 "if the condition is constant, you may not assume the loop terminates"。因此,并且根据标准授予没有可观察的行为(如标准中定义的),我只会争论一致性 - 如果编译器正在优化一个循环,因为它假设它终止,它不应该优化以下语句。
哎呀n1528 如果我没看错的话,这些都是未定义的行为。具体
A major issue for doing so is that it allows code to move across a potentially non-terminating loop
从这里我认为它只能转变成对我们想要(预期的?)而不是允许的讨论。
循环没有副作用,所以可以优化掉。该循环实际上是零工作单元的无限次迭代。这在数学和逻辑中是未定义的,并且标准没有说明如果每件事都可以在零时间内完成,是否允许实现完成无限多的事情。 Clang 的解释将无穷大乘以零视为零而不是无穷大是完全合理的。该标准没有说明如果循环中的所有工作实际上都已完成,无限循环是否可以结束。
允许编译器优化掉标准中定义的任何不可观察的行为。这包括执行时间。不需要保留这样一个事实,即循环如果不进行优化,将花费无限长的时间。允许将其更改为更短的 运行 时间——事实上,这是大多数优化的重点。您的循环已优化。
即使 clang 天真地翻译了代码,您也可以想象一个优化 CPU 可以在前一次迭代所用时间的一半时间内完成每次迭代。这实际上会在有限的时间内完成无限循环。这样的优化CPU是否违反标准?如果说优化 CPU 太擅长优化就会违反标准,这似乎很荒谬。编译器也是如此。
您需要插入一个可能导致副作用的表达式。
最简单的解决方案:
static void die() {
while(1)
__asm("");
}
C11 标准是这样说的,6.8.5/6:
An iteration statement whose controlling expression is not a constant expression,156) that performs no input/output operations, does not access volatile objects, and performs no synchronization or atomic operations in its body, controlling expression, or (in the case of a for statement) its expression-3, may be assumed by the implementation to terminate.157)
两个脚注不是规范性的,但提供了有用的信息:
156) An omitted controlling expression is replaced by a nonzero constant, which is a constant expression.
157) This is intended to allow compiler transformations such as removal of empty loops even when termination cannot be proven.
在您的例子中,while(1)
是一个 crystal 明确的常量表达式,因此它可能 不会 被实现假设为终止。这样的实现将无可救药地被破坏,因为 "for-ever" 循环是一种常见的编程结构。
然而,据我所知,循环后 "unreachable code" 会发生什么,并没有明确定义。然而,clang 确实表现得很奇怪。机器代码与 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
clang 9.0.0 -O3 -std=c11 -pedantic-errors
main: # @main
push rax
mov edi, offset .Lstr
call puts
.Lstr:
.asciz "begin"
gcc 生成循环,clang 只是跑到树林里,并以错误 255 退出。
我倾向于这是 clang 的不合规行为。因为我试图像这样进一步扩展您的示例:
#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
将在第一次执行时 return 0,所以这个程序应该直接进入 while(1)
并停在那里,只打印 "begin" (假设 \n 刷新标准输出)。 gcc 会发生这种情况。
如果简单地删除循环,它应该打印 "begin" 2 次然后打印 "unreachable"。然而,在 clang 上(godbolt),它打印 "begin" 1 次,然后在 returning 退出代码 0 之前打印 "unreachable"。无论你怎么说,这都是完全错误的。
我在这里找不到声明未定义行为的案例,所以我认为这是 clang 中的一个错误。无论如何,这种行为使 clang 对嵌入式系统等程序 100% 无用,在嵌入式系统中,您必须能够依赖挂起程序的永恒循环(在等待看门狗等时)。
我会唱反调并争辩说该标准并未明确禁止编译器优化无限循环。
An iteration statement whose controlling expression is not a constant expression,156) that performs no input/output operations, does not access volatile objects, and performs no synchronization or atomic operations in its body, controlling expression, or (in the case of a for statement) its expression-3, may be assumed by the implementation to terminate.157)
我们来分析一下。满足特定条件的迭代语句可以假定终止:
if (satisfiesCriteriaForTerminatingEh(a_loop))
if (whatever_reason_or_just_because_you_feel_like_it)
assumeTerminates(a_loop);
这并没有说明如果不满足标准会发生什么,并且只要遵守标准的其他规则,就不会明确禁止循环可能终止的假设。
do { } while(0)
或 while(0){}
毕竟是不满足允许编译器随心所欲地假设它们终止但它们显然确实终止的标准的所有迭代语句(循环) .
但是编译器可以优化while(1){}
出来吗?
5.1.2.3p4 说:
In the abstract machine, all expressions are evaluated as specified by the semantics. An actual implementation need not evaluate part of an expression if it can deduce that its value is not used and that no needed side effects are produced (including any caused by calling a function or accessing a volatile object).
这里提到的是表达式,而不是语句,所以它不是 100% 令人信服,但它肯定允许这样的调用:
void loop(void){ loop(); }
int main()
{
loop();
}
被跳过。有趣的是,clang does skip it, and gcc doesn't.
这是一个 Clang 错误
... 内联包含无限循环的函数时。当 while(1);
直接出现在 main 中时,行为是不同的,这对我来说很臭。
有关摘要和链接,请参阅 @Arnavion's answer。这个答案的其余部分是在我确认这是一个错误之前写的,更不用说一个已知的错误了。
回答标题问题:如何制作一个不会被优化掉的无限空循环?? -
将 die()
设为宏,而不是函数 ,以解决 Clang 3.9 及更高版本中的此错误。 (早期的 Clang 版本 keeps the loop or emits a call
to a non-inline version of the function with the infinite loop.) That appears to be safe even if the print;while(1);print;
function inlines into its caller (Godbolt)。 -std=gnu11
与 -std=gnu99
没有任何改变。
如果您只关心 GNU C,P__J__'s __asm__("");
inside the loop also works, and shouldn't hurt optimization of any surrounding code for any compilers that understand it. GNU C Basic asm statements are implicitly volatile
,那么这算作一个可见的 side-effect,它必须 "execute" 与在 C 抽象机中一样多。 (是的,Clang 实现了 C 的 GNU 方言,如 GCC 手册所述。)
有些人认为优化空的无限循环可能是合法的。我不同意1,但即使我们接受这一点,也不能也 Clang 假设是合法的循环后的语句无法访问, 并让执行从函数末尾进入下一个函数,或进入解码为随机指令的垃圾。
(对于 Clang++ 来说是 standards-compliant(但仍然不是很有用);没有任何副作用的无限循环是 C++ 中的 UB,但不是 C。
Is while(1); undefined behavior in C? UB 让编译器基本上为执行路径上肯定会遇到 UB 的代码发出任何内容。循环中的 asm
语句将避免此 UB for C++。但实际上,Clang 编译为 C++ 不会删除 constant-expression 无限空循环,除非内联,与编译为 C 时相同。)
手动内联 while(1);
改变了 Clang 编译它的方式:asm 中存在无限循环。 这是我们对 rules-lawyer POV 的期望。
#include <stdio.h>
int main() {
printf("begin\n");
while(1);
//infloop_nonconst(1);
//infloop();
printf("unreachable\n");
}
On the Godbolt compiler explorer,Clang 9.0 -O3 编译为适用于 x86-64 的 C (-xc
):
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"
具有相同选项的相同编译器编译一个 main
,它首先调用 infloop() { while(1); }
到相同的 puts
,但之后就停止为 main
发出指令观点。因此,正如我所说,执行只是从函数的末尾开始,进入下一个函数(但堆栈未对齐函数入口,因此它甚至不是有效的尾调用)。
有效选项是
- 发出一个
label: jmp label
无限循环 - 或者(如果我们接受无限循环可以被移除)发出另一个调用来打印第二个字符串,然后
return 0
从main
.
崩溃或以其他方式继续而不打印 "unreachable" 显然对于 C11 实现来说是不行的,除非有我没有注意到的 UB。
脚注 1:
郑重声明,我同意 @Lundin's answer which cites the standard 的证据,即 C11 不允许假设 constant-expression 无限循环终止,即使它们是空的(没有 I/O 、volatile、同步或其他可见的 side-effects).
这是一组条件,可以将循环编译为空 asm 循环 用于正常 CPU。 (即使 body 在源代码中不为空,在循环为 运行ning 时,没有 data-race UB 的情况下,对变量的赋值对其他线程或信号处理程序是不可见的。所以如果需要,符合规范的实现可以删除此类循环体。然后就剩下循环本身是否可以删除的问题。ISO C11 明确表示不可以。)
鉴于 C11 将这种情况作为实现不能假设循环终止的情况(并且它不是 UB),很明显他们打算在 run-time 处出现循环。一个以 CPUs 为目标的实现,其执行模型不能在有限时间内完成无限量的工作,没有理由删除一个空的常量无限循环。或者甚至在一般情况下,确切的措辞是关于它们是否可以 "assumed to terminate" 。如果循环无法终止,则意味着后面的代码无法访问,无论 what arguments you make 关于数学和无穷大以及在某个假设的机器上做无限量的工作需要多长时间。
此外,Clang 不仅仅是一个符合 ISO C 的 DeathStation 9000,它旨在用于 real-world low-level 系统编程,包括内核和嵌入式东西. 因此,无论您是否接受关于 C11 允许 删除 while(1);
的论点,Clang 想要实际这样做是没有意义的。如果你写 while(1);
,那可能不是偶然的。删除意外无限结束的循环(使用 运行 时间变量控制表达式)可能很有用,编译器这样做很有意义。
您很少想一直循环到下一个中断,但是如果您用 C 语言编写它,那绝对是您期望发生的事情。 (还有 doe 发生在 GCC 和 Clang 中,但当无限循环在包装函数内时 Clang 除外。
例如,在原始 OS 内核中,当调度程序没有要 运行 的任务时,它可能 运行 空闲任务。第一个实现可能是 while(1);
.
或者对于没有任何 power-saving 空闲功能的硬件,这可能是唯一的实现。 (直到 2000 年代初期,我认为这在 x86 上并不少见。尽管 hlt
指令确实存在,但 IDK 如果它节省了有意义的电量,直到 CPUs 开始有 low-power 空闲州。)
以下似乎对我有用:
#include <stdio.h>
__attribute__ ((optnone))
static void die(void) {
while (1) ;
}
int main(void) {
printf("begin\n");
die();
printf("unreachable\n");
}
在 godbolt
明确告诉 Clang 不要优化一个函数会导致按预期发出无限循环。希望有一种方法可以选择性地禁用特定的优化,而不是像那样将它们全部关闭。不过,Clang 仍然拒绝为第二个 printf
发出代码。为了强制它这样做,我不得不进一步将 main
中的代码修改为:
volatile int x = 0;
if (x == 0)
die();
您似乎需要禁用无限循环函数的优化,然后确保有条件地调用无限循环。在现实世界中,反正后者几乎总是如此。
其他答案已经涵盖了使用内联汇编语言或其他副作用使 Clang 发出无限循环的方法。我只是想确认这确实是一个编译器错误。具体来说,它是 a long-standing LLVM bug - 它将“所有没有副作用的循环都必须终止”的 C++ 概念应用于不应终止的语言,例如 C。该错误最终在 LLVM 12 中得到修复。
例如,the Rust programming language also allows infinite loops and uses LLVM as a backend, and it had this same issue.
LLVM 12 添加了一个 mustprogress
属性,前端可以忽略该属性以指示何时函数不一定 return,并且更新了 clang 12 以说明它。您可以看到您的示例编译正确 with clang 12.0.0 whereas it did not with clang 11.0.1
郑重声明,Clang 也对 goto
:
static void die() {
nasty:
goto nasty;
}
int main() {
int x; printf("begin\n");
die();
printf("unreachable\n");
}
它产生与问题中相同的输出,即:
main: # @main
push rax
mov edi, offset .Lstr
call puts
.Lstr:
.asciz "begin"
我看不出任何 C11 允许的方式来阅读它,它只说:
6.8.6.1(2) A
goto
statement causes an unconditional jump to the statement prefixed by the named label in the enclosing function.
因为 goto
不是 "iteration statement"(6.8.5 列出了 while
、do
和 for
)与特殊的 [=37= 无关] 放纵适用,但你想阅读它们。
每个原始问题的 Godbolt link 编译器是 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
标志:-g -o output.s -masm=intel -S -fdiagnostics-color=always -O2 -std=c11 example.c
一个符合规范的实现可能,而且许多实际的实现,对一个程序可以执行多长时间或者它可以执行多少条指令强加任意限制,并且如果这些限制被违反或者 - 在 "as-if" rule--如果它决定了他们不可避免地会被违反。如果一个实现可以成功地处理至少一个名义上执行 N1570 5.2.4.1 中列出的所有限制而没有达到任何翻译限制的程序,则限制的存在、记录的范围以及超过限制的影响是标准管辖范围之外的所有实施质量问题。
我认为该标准的意图非常明确,即编译器不应假定没有副作用的 while(1) {}
循环或 break
语句会终止。与某些人可能认为的相反,该标准的作者并没有邀请编译器编写者变得愚蠢或迟钝。一个符合规范的实现可能有助于决定终止任何程序,如果不被中断,该程序将执行比宇宙中的原子更多的无副作用指令,但高质量的实现不应该基于任何假设执行这样的操作终止,而是基于这样做可能有用,并且不会(与 clang 的行为不同)比无用更糟糕。
看来这是Clang编译器的一个bug。如果 die()
函数没有任何强制成为静态函数,则取消 static
并使其成为 inline
:
#include <stdio.h>
inline void die(void) {
while(1)
;
}
int main(void) {
printf("begin\n");
die();
printf("unreachable\n");
}
它在使用 Clang 编译器编译时按预期工作,并且也是可移植的。
Compiler Explorer (godbolt.org) - clang 9.0.0 -O3 -std=c11 -pedantic-errors
main: # @main
push rax
mov edi, offset .Lstr
call puts
.LBB0_1: # =>This Inner Loop Header: Depth=1
jmp .LBB0_1
.Lstr:
.asciz "begin"
很抱歉,如果情况并非如此荒谬,我偶然发现了这个 post 并且我知道因为我多年来使用 Gentoo Linux 发行版,如果您希望编译器不优化您的你应该使用-O0(零)的代码。我对此很好奇,并编译并 运行 上面的代码,并且循环无限期地进行下去。使用 clang-9 编译:
cc -O0 -std=c11 test.c -o test