【问题标题】:How do I make an infinite empty loop that won't be optimized away?如何制作一个不会被优化的无限空循环?
【发布时间】:2020-05-12 12:04:20
【问题描述】:

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,然后在一条非法指令上崩溃ud2 陷阱放置在die() 之后)。 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 在优化开启的情况下输出正确的、无内存访问的无限循环?

【问题讨论】:

  • 评论不用于扩展讨论;这个对话是moved to chat
  • 没有不涉及副作用的便携式解决方案。如果您不想要内存访问,您最好的希望是注册 volatile unsigned char;但是寄存器在 C++17 中消失了。
  • 也许这不在问题的范围内,但我很好奇你为什么要这样做。当然还有其他方法可以完成你的真正任务。或者这只是学术性质的?
  • @Cruncher:任何特定尝试运行程序的效果可能是有用的,基本上是无用的,或者比无用更糟糕。导致程序陷入无限循环的执行可能是无用的,但仍然比编译器可能替代的其他行为更可取。
  • @Cruncher:因为代码可能在没有exit() 概念的独立上下文中运行,并且因为代码可能发现了无法保证继续执行的效果的情况不会比没用更糟。跳到自我循环是处理这种情况的一种非常糟糕的方法,但它可能仍然是处理糟糕情况的最佳方法。

标签: c clang language-lawyer compiler-optimization


【解决方案1】:

C11 标准是这样说的,6.8.5/6:

控制表达式不是常量表达式的迭代语句,156) 不执行输入/输出操作,不访问 volatile 对象,并且不执行 其主体、控制表达式或(在 for 语句的情况下)其表达式 3 中的同步或原子操作,可以由实现假定为 终止。157)

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

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

157) 这旨在允许编译器转换,例如删除空循环,即使在 无法证明终止。

在您的情况下,while(1) 是一个非常清晰的常量表达式,因此实现可能假定它会终止。这样的实现将被彻底破坏,因为“永远”循环是一种常见的编程结构。

据我所知,循环之后“无法访问的代码”会发生什么,但没有明确定义。但是,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 将在第一次执行时返回 0,所以这个程序应该直接撞到 while(1) 并停在那里,只打印“开始”(假设 \n 刷新标准输出)。这发生在 gcc 上。

如果循环被简单地删除,它应该打印“begin”2 次,然后打印“unreachable”。然而,在 clang (godbolt) 上,它会打印“开始”1 次,然后在返回退出代码 0 之前打印“无法访问”。无论如何,这都是错误的。

我在这里找不到声称未定义行为的案例,所以我认为这是 clang 中的一个错误。无论如何,这种行为使 clang 100% 对嵌入式系统等程序毫无用处,在这些程序中,您只需要能够依靠永恒的循环来挂起程序(等待看门狗等)。

【讨论】:

  • 我不同意“这是一个非常清晰的常量表达式,因此实现可能不会假定它会终止”。这确实涉及到挑剔的语言律师,但6.8.5/6 的形式是如果(这些)那么你可以假设(这个)。这并不意味着如果不是(这些)你可能不会假设(这个)。它是仅在满足条件时的规范,而不是在未满足条件时,您可以根据标准做任何您想做的事情。如果没有可观察的......
  • @kabanus 引用的部分是一个特例。如果不是(特殊情况),请像​​往常一样评估和排序代码。如果您继续阅读同一章,除了引用的特殊情况外,控制表达式将按照每个迭代语句的指定(“由语义指定”)进行评估。它遵循与任何值计算的评估相同的规则,这些规则是有序且定义明确的。
  • 我同意,但你不会感到惊讶的是,在 int z=3; int y=2; int x=1; printf("%d %d\n", x, z); 中,程序集中没有 2,所以在空无意义的意义上,x 不是在 y 之后分配的,而是在之后分配的z 由于优化。因此,从您的最后一句话开始,我们遵循常规规则,假设 while 停止(因为我们没有更好地受到约束),并留在最后的“无法访问”打印中。现在,我们优化了那个无用的语句(因为我们不知道更好)。
  • @MSalters 我的一个 cmets 已被删除,但感谢您的输入 - 我同意。我的评论说的是,我认为这是辩论的核心——while(1);int y = 2; 声明相同,就我们允许优化的语义而言,即使它们的逻辑保留在源代码中。从n1528开始,我的印象是它们可能是一样的,但是由于比我更有经验的人在争论另一种方式,而且显然这是一个官方错误,然后就标准中的措辞是否明确进行哲学辩论,这个论点就没有实际意义了。
  • “这样的实现会被彻底破坏,因为‘永远’循环是一种常见的编程结构。” ——我理解这种情绪,但这个论点是有缺陷的,因为它可以同样应用于 C++,但是优化这个循环的 C++ 编译器不会被破坏,而是符合要求。
【解决方案2】:

您需要插入一个可能导致副作用的表达式。

最简单的解决方案:

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

Godbolt link

【讨论】:

  • 只是说“这是clang中的一个错误”就足够了。不过,在我大喊“bug”之前,我想先在这里尝试一些事情。
  • @Lundin 我不知道这是否是一个错误。在这种情况下,标准在技术上并不精确
  • 幸运的是,GCC 是开源的,我可以编写一个编译器来优化您的示例。对于您现在和将来提出的任何示例,我都可以这样做。
  • @nneonneo:GNU C 基本 asm 语句隐含为 volatile,就像没有输出操作数的扩展 Asm 语句一样。如果你写了asm("" : "=r"(dummy)); 并且没有使用dummy 结果,它被优化掉。您需要asm volatile 告诉编译器存在副作用(或读取变化的输入,如 rdtsc)以及产生输出的直接影响。所以是的,副作用不能被优化掉,但关键是编译器是否假设有副作用! gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html#Volatile
  • @ThomasWeller:GCC 开发者不会接受优化掉这个循环的补丁;它会违反记录的=保证的行为。请参阅我之前的评论:asm("") 隐含地为asm volatile("");,因此 asm 语句必须像在抽象机器 gcc.gnu.org/onlinedocs/gcc/Basic-Asm.html 中那样运行多次。 (请注意,包含任何内存或寄存器的副作用安全;如果您想读取或写入您曾经从 C 访问过的内存,则需要带有 "memory" clobber 的扩展 asm。基本asm 只对 asm("mfence")cli 之类的东西安全。)
【解决方案3】:

其他答案已经涵盖了使用内联汇编语言或其他副作用使 Clang 发出无限循环的方法。我只是想确认这确实是一个编译器错误。具体来说,它是a long-standing LLVM bug——它将“所有没有副作用的循环都必须终止”的 C++ 概念应用于不应该终止的语言,例如 C。该错误最终在 LLVM 12 中得到修复。

例如the Rust programming language也允许无限循环并使用LLVM作为后端,它had this same issue.

LLVM 12 添加了一个 mustprogress 属性,前端可以省略该属性以指示函数何时不一定返回,并且更新了 clang 12 以解决此问题。您可以看到您的示例正确编译 with clang 12.0.0 而不是 with clang 11.0.1

【讨论】:

  • 没有什么比十年前的错误的味道...有多个建议的修复和补丁...但仍未修复。
  • @IanKemp:他们现在要修复这个错误需要承认他们已经花了十年时间来修复这个错误。最好抱有希望,标准会改变以证明他们的行为是正当的。当然,即使标准确实发生了变化,这仍然不能证明他们的行为是正当的,除非在人们认为标准的变化表明标准的早期行为授权是一个应该追溯纠正的缺陷的人眼中。
  • LLVM 添加了 sideeffect 操作(在 2017 年)并希望前端自行决定将该操作插入到循环中,因此它已被“修复”。 LLVM 必须为循环选择 some 默认值,并且它碰巧选择了与 C++ 的行为一致的循环,有意或无意。当然,还有一些优化工作需要做,比如将连续的sideeffect ops 合并为一个。 (这就是阻止 Rust 前端使用它的原因。)因此,在此基础上,错误出现在前端(clang)中,它没有在循环中插入操作。
  • @Arnavion:有没有办法表明操作可能会被推迟,除非或直到使用结果,但是如果数据会导致程序无休止地循环,试图继续过去的数据依赖关系会使程序比没用还糟糕?不得不添加虚假的副作用来阻止以前有用的优化以防止优化器使程序变得比无用更糟糕,这听起来不像是提高效率的秘诀。
  • 该讨论可能属于 LLVM / clang 邮件列表。 FWIW 添加操作的 LLVM 提交确实也教授了几个关于它的优化通道。此外,Rust 尝试在每个函数的开头插入 sideeffect ops,并且没有看到任何运行时性能下降。唯一的问题是 compile time 回归,显然是由于我在之前的评论中提到的连续操作缺乏融合。
【解决方案4】:

这是一个 Clang 错误

... 内联包含无限循环的函数时。当while(1); 直接出现在 main 中时,行为是不同的,这对我来说闻起来很臭。

有关摘要和链接,请参阅 @Arnavion's answer。这个答案的其余部分是在我确认这是一个错误之前写的,更不用说一个已知的错误了。


回答标题问题:如何创建一个不会被优化掉的无限空循环?? -
die() 设为宏而非函数,以解决 Clang 3.9 及更高版本中的此错误。 (早期的 Clang 版本是 keeps the loop or emits a call 到具有无限循环的函数的非内联版本。)即使 print;while(1);print; 函数内联到 its 调用者(Godbolt )。 -std=gnu11-std=gnu99 没有任何改变。

如果您只关心 GNU C,循环内的P__J__'s __asm__(""); 也可以工作,并且不应该损害任何理解它的编译器对任何周围代码的优化。 GNU C Basic asm 语句是implicitly volatile,所以这算作一个可见的副作用,它必须像在 C 抽象机器中一样多次“执行”。 (是的,Clang 实现了 C 的 GNU 方言,如 GCC 手册所述。)


有些人认为优化掉一个空的无限循环可能是合法的。我不同意1,但即使我们接受这一点,Clang 不能在循环后假设语句不可访问是合法的, 并让执行从函数末尾落入下一个函数,或落入解码为随机指令的垃圾中。

(这将符合 Clang++ 的标准(但仍然不是很有用);没有任何副作用的无限循环是 C++ 中的 UB,但不是 C。
Is while(1); undefined behavior in C? UB 让编译器基本上可以为代码发出任何内容在肯定会遇到 UB 的执行路径上。循环中的 asm 语句将避免 C++ 的此 UB。但实际上,编译为 C++ 的 Clang 不会删除常量表达式无限空循环,除非内联时,与编译为 C 时。)


手动内联 while(1); 改变了 Clang 编译它的方式:asm 中存在无限循环。这是我们对规则律师 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"

具有相同选项的相同编译器将调用infloop() { while(1); }main 编译为相同的第一个puts,但随后停止为main 发出指令。所以正如我所说,执行只是从函数的末尾落下,进入下一个函数(但堆栈未对齐函数入口,因此它甚至不是有效的尾调用)。

有效的选项是

  • 发出label: jmp label 无限循环
  • 或者(如果我们接受可以移除无限循环)发出另一个调用以打印第二个字符串,然后从main 发出return 0

对于 C11 实现而言,崩溃或以其他方式继续而不打印“无法访问”显然是不行的,除非有我没有注意到的 UB。


脚注 1:

作为记录,我同意 @Lundin's answer which cites the standard 的证据,即 C11 不允许假设常量表达式无限循环终止,即使它们是空的(没有 I/O、易失性、同步或其他可见的副作用)。

这是一组条件,可以让循环编译为普通 CPU 的空 asm 循环。 (即使源代码中的主体不是空的,在循环运行时,没有数据竞争 UB 的其他线程或信号处理程序也无法看到对变量的赋值。因此,如果需要,符合要求的实现可以删除此类循环主体到。那么这就留下了循环本身是否可以被删除的问题。ISO C11 明确表示不能。)

鉴于 C11 将这种情况单选为实现不能假设循环终止(并且它不是 UB)的情况,很明显他们打算在运行时出现循环。一个以 CPU 为目标的实现,其执行模型不能在有限时间内完成无限量的工作,没有理由移除一个空的常量无限循环。或者甚至在一般情况下,确切的措辞是关于它们是否可以“假定终止”。如果循环无法终止,则意味着后面的代码无法访问,无论 what arguments you make 关于数学和无穷大,以及在某个假设的机器上完成无限量的工作需要多长时间。

除此之外,Clang 不仅仅是一个符合 ISO C 标准的 DeathStation 9000,它旨在用于现实世界的低级系统编程,包括内核和嵌入式东西。 所以无论是不是您接受有关 C11 允许 删除 while(1); 的论点,Clang 想要实际这样做是没有意义的。如果你写while(1);,那可能不是意外。删除意外导致无限的循环(使用运行时变量控制表达式)可能很有用,编译器这样做是有意义的。

您很少希望在下一次中断之前一直旋转,但如果您用 C 语言编写它,那绝对是您所期望的。 (以及在 GCC 和 Clang 中会发生什么,当无限循环位于包装函数内部时,Clang 除外)。

例如,在原始操作系统内核中,当调度程序没有要运行的任务时,它可能会运行空闲任务。第一个实现可能是while(1);

或者对于没有任何节能空闲功能的硬件,这可能是唯一的实现。 (直到 2000 年代初,我认为这在 x86 上并不罕见。虽然 hlt 指令确实存在,但 IDK 如果它在 CPU 开始处于低功耗空闲状态之前节省了大量的电力。)

【讨论】:

  • 出于好奇,真的有人在嵌入式系统中使用clang吗?我从未见过它,而且我只使用嵌入式。 gcc 只是“最近”(10 年前)进入嵌入式市场,我怀疑地使用它,最好是低优化并且总是使用-ffreestanding -fno-strict-aliasing。它适用于 ARM,也许适用于旧版 AVR。
  • @Lundin:关于嵌入式的 IDK,但是是的,人们确实使用 clang 构建内核,至少有时是 Linux。大概还有适用于 MacOS 的 Darwin。
  • bugs.llvm.org/show_bug.cgi?id=965 这个错误看起来很相关,但我不确定我们在这里看到的是什么。
  • @lundin - 我很确定我们在整个 90 年代都使用 GCC(和许多其他工具包)进行嵌入式工作,以及诸如 VxWorks 和 PSOS 之类的 RTOS。我不明白你为什么说GCC最近才进入嵌入式市场。
  • @JeffLearman 最近成了主流,那?无论如何,gcc 严格别名的惨败只发生在 C99 引入之后,而且它的新版本在遇到严格的别名违规时似乎也不再是香蕉。尽管如此,每当我使用它时,我仍然持怀疑态度。至于clang,最新的版本在永恒循环方面显然是彻底坏掉了,所以不能用于嵌入式系统。
【解决方案5】:

为了记录,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) goto 语句导致无条件跳转到以封闭函数中命名标签为前缀的语句。

由于goto 不是“迭代声明”(6.8.5 列出了whiledofor),因此对于特殊的“假定终止”放纵不适用,但是您想阅读它们。

每个原始问题的 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

标志:-g -o output.s -masm=intel -S -fdiagnostics-color=always -O2 -std=c11 example.c

【讨论】:

  • 一个符合标准的实现可能对执行时间或 CPU 周期有一个未记录的翻译限制,如果超过,或者如果程序输入不可避免地超过限制,可能会导致任意行为。这些事情是执行质量问题,超出了标准的管辖范围。奇怪的是,clang 的维护者如此坚持他们的权利来产生一个低质量的实现,但标准确实允许这样做。
  • @supercat 感谢您的评论...为什么超出翻译限制会导致翻译阶段失败并拒绝执行?另外:“5.1.1.3 诊断如果预处理翻译单元或翻译单元包含违反任何语法规则或约束 ...”。我看不出执行阶段的错误行为是如何符合的。
  • 如果必须在构建时解决所有实现限制,则该标准将完全不可能实现,因为可以编写一个严格符合的程序,该程序需要的堆栈字节数超过宇宙中的原子数.目前尚不清楚是否应将运行时限制与“翻译限制”混为一谈,但这样的让步显然是必要的,而且没有其他类别可以归类。
  • 我在回复您关于“翻译限制”的评论。当然也有执行限制,我承认我不明白你为什么建议它们应该与翻译限制混为一谈,或者为什么你说这是必要的。我只是看不出有任何理由说 nasty: goto nasty 可以符合要求并且在用户或资源耗尽干预之前不会旋转 CPU。
  • 标准没有提到我能找到的“执行限制”。函数调用嵌套之类的事情通常由堆栈分配处理,但是将函数调用限制为 16 深度的符合实现可以构建每个函数的 16 个副本,并且在 foo() 中对 bar() 的调用被作为调用处理从__1foo__2bar,从__2foo__3bar 等等,从__16foo__launch_nasal_demons,这将允许所有自动对象被静态分配,并且通常 将“运行时”限制转换为翻译限制。
【解决方案6】:

我将扮演魔鬼的拥护者并争辩说该标准并未明确禁止编译器优化无限循环。

控制表达式不是常量的迭代语句 表达式,156)不执行输入/输出操作,不 访问 volatile 对象,并且不执行同步或原子操作 其主体中的操作、控制表达式或(在 for 语句)它的表达式 3,可以由实现假定 终止。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 说:

在抽象机中,所有表达式都按照 语义。一个实际的实现不需要评估一个 表达式,如果它可以推断出它的值没有被使用并且没有 产生了所需的副作用(包括任何由调用 函数或访问 volatile 对象)。

这里提到的是表达式,而不是语句,所以它不是 100% 有说服力,但它确实允许这样的调用:

void loop(void){ loop(); }

int main()
{
    loop();
}

被跳过。有趣的是,clang does skip it, and gcc doesn't

【讨论】:

  • @Lundin 所以while(1){}1 评估与{} 评估交织在一起的无限序列,但是在标准中哪里说这些评估需要非零 时间?我猜,gcc 行为更有用,因为您不需要涉及内存访问的技巧或语言之外的技巧。但我不相信标准禁止在clang中进行这种优化。如果意图使 while(1){} 不可优化,则标准应该明确说明它,并且无限循环应该在 5.1.2.3p2 中列为可观察到的副作用。
  • 如果您将1 条件视为值计算,我认为它已指定。执行时间无关紧要 - 重要的是 while(A){} B; 可能被完全优化掉,没有优化到 B; 并且没有重新排序到 B; while(A){}。引用 C11 抽象机,强调我的:“在表达式 A 和 B 的求值之间存在一个序列点意味着 每个值计算 和副作用 与 A 相关联在每个价值计算和副作用与B相关。" A 的值被清楚地使用(由循环)。
  • +1 即使在我看来“执行无限期挂起而没有任何输出”在“副作用”的任何定义中都是一个“副作用”,它是有意义的,并且不仅有用真空中的标准,这有助于解释对某人有意义的心态。
  • @PSkocik:我不明白 1) 的意义。我认为这对每个人来说已经很明显了。当然,您可以在 C 中编写非无限循环。无论如何,至于 2),是的,我接受关于删除无限循环的一些论点。但是您是否错过了 clang also 将后面的语句视为无法访问并使得 asm 刚刚脱离函数末尾的事实(甚至不是 ret)?删除无限循环将其后的语句视为不可达是不合法的,除非该执行路径包含UB。见my answer
  • Near "优化无限循环":不完全清楚 "it" 是指标准还是编译器 - 也许改写?鉴于 "虽然它可能应该" 而不是 "虽然它可能不应该",它可能是 "it" 所指的标准.
【解决方案7】:

我一直坚信这只是一个普通的老错误。出于我之前的一些推理,我将我的测试留在下面,特别是对标准委员会讨论的参考。


我认为这是未定义的行为(见结尾),而 Clang 只有一个实现。 GCC 确实如您所愿,只优化了unreachable print 语句,但离开了循环。在组合内联并确定它可以用循环做什么时,Clang 是如何奇怪地做出决定的。

这种行为更加奇怪 - 它删除了最终打印,因此“看到”了无限循环,但随后也摆脱了循环。

据我所知,情况更糟。删除我们得到的内联:

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 讨论之后,我得出标准并没有说“如果条件不变,您可能不会假设循环终止”。因此,并且根据标准授予没有可观察的行为(如标准中定义的那样),我只为一致性争论 - 如果编译器正在优化循环,因为它假设它终止,它不应该优化以下语句。

哎呀n1528 如果我没看错的话,这些都是未定义的行为。具体

这样做的一个主要问题是它允许代码在潜在的非终止循环中移动

从这里开始,我认为只能讨论我们想要(预期?)而不是允许什么。

【讨论】:

  • 评论不用于扩展讨论;这个对话是moved to chat
  • Re "plain all bug":你的意思是"plain old bug"吗?
  • @PeterMortensen "ole" 我也可以。
【解决方案8】:

这似乎是 Clang 编译器中的一个错误。如果没有强制 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"

【讨论】:

  • static inline 呢?
【解决方案9】:

以下似乎对我有用:

#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();

您似乎需要禁用无限循环函数的优化,然后确保有条件地调用无限循环。在现实世界中,后者几乎总是如此。

【讨论】:

  • 如果循环确实永远持续下去,则不需要生成第二个printf,因为在这种情况下,第二个printf 确实无法访问,因此可以删除。 (Clang 的错误在于检测不可达性,然后删除循环以达到不可达代码)。
  • GCC 文档 __attribute__ ((optimize(1))),但 clang 将其忽略为不受支持:godbolt.org/z/4ba2HMgcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html
【解决方案10】:

一个符合标准的实现可能,而且许多实际的实现,对程序可以执行多长时间或它将执行多少指令施加任意限制,并且如果违反这些限制或 - 根据“as- if”规则——如果它确定它们将不可避免地被违反。假设一个实现可以成功处理至少一个程序,该程序名义上执行 N1570 5.2.4.1 中列出的所有限制而不达到任何转换限制,限制的存在、记录的范围以及超出它们的影响,是标准管辖范围之外的所有实施质量问题。

我认为标准的意图非常明确,编译器不应假定没有副作用的while(1) {} 循环或break 语句将终止。与某些人的想法相反,该标准的作者并没有邀请编译器编写者变得愚蠢或迟钝。符合要求的实现可能有助于决定终止任何程序,如果不中断,执行比宇宙中的原子更多的无副作用指令,但质量实现不应该基于任何假设来执行这样的操作终止,而是基于这样做可能有用,并且不会(与 clang 的行为不同)比无用更糟糕。

【讨论】:

    【解决方案11】:

    循环没有副作用,因此可以优化。循环实际上是零工作单元的无限次迭代。这在数学和逻辑中是未定义的,并且标准没有说明如果每件事都可以在零时间内完成,是否允许实现完成无限数量的事情。 Clang 的解释在将无穷乘以零视为零而不是无穷时是完全合理的。标准并没有说明如果循环中的所有工作实际上都已完成,无限循环是否可以结束。

    允许编译器优化标准中定义的任何不可观察的行为。这包括执行时间。不需要保留这样一个事实,即如果不进行优化,循环将花费无限时间。允许将其更改为更短的运行时间——事实上,这是大多数优化的重点。您的循环已优化。

    即使 clang 天真地翻译了代码,您也可以想象一个优化的 CPU 可以在上一次迭代所用时间的一半内完成每次迭代。这将在有限的时间内完成无限循环。这样的优化CPU是否违反标准?说一个优化过的CPU会违反标准,这似乎很荒谬。编译器也是如此。

    【讨论】:

    • 评论不用于扩展讨论;这个对话是moved to chat
    • 根据您的经验(从您的个人资料)来看,我只能得出结论,这篇文章是恶意写的,只是为了保护编译器。您在认真地争辩说,可以优化花费无限时间的东西以在一半时间内执行。这在各个层面都是荒谬的,你知道的。
    • @pipe:我认为 clang 和 gcc 的维护者希望标准的未来版本将允许他们的编译器的行为,并且这些编译器的维护者将能够假装这样的变化仅仅是对标准中长期存在的缺陷的更正。例如,他们就是这样对待 C89 的 Common Initial Sequence 保证的。
    • @S.S.Anne:嗯...我认为这不足以阻止 gcc 和 clang 从指针相等比较的结果中得出的一些不合理的推论。
    • @supercat 还有其他吨。
    【解决方案12】:

    如果事实并非如此,我很抱歉,我偶然发现了这篇文章,我知道,因为我多年来使用 Gentoo Linux 发行版,如果你希望编译器不优化你的代码,你应该使用 -O0(Zero)。我对此很好奇,并编译并运行了上面的代码,并且循环无限期地进行。使用 clang-9 编译:

    cc -O0 -std=c11 test.c -o test
    

    【讨论】:

    • 重点是在启用优化的情况下进行无限循环。
    【解决方案13】:

    空的while 循环对系统没有任何副作用。

    因此 Clang 将其删除。有一些“更好”的方法可以实现预期的行为,迫使你更明显地表达自己的意图。

    while(1); 是 baaadd。

    【讨论】:

    • 在许多嵌入式结构中,没有abort()exit() 的概念。如果出现一种情况,即函数确定(可能是由于内存损坏)继续执行比危险更糟糕,嵌入式库的常见默认行为是调用执行while(1); 的函数。让编译器拥有 options 来替代更有用 的行为可能很有用,但是任何无法弄清楚如何将这样一个简单的构造视为障碍的编译器编写者继续执行程序是无法信任复杂优化的。
    • 有什么方法可以更明确地表达你的意图吗?优化器可以优化您的程序,删除不执行任何操作的冗余循环是一种优化。这确实是数学世界的抽象思维与更应用的工程世界之间的哲学差异。
    • 大多数程序都有一组它们应该在可能的情况下执行的有用操作,以及一组在任何情况下都绝对不能执行的不如无用的操作。许多程序在任何特定情况下都有一组可接受的行为,其中一个行为,如果执行时间不可观察,则始终是“等待一些任意行为,然后从集合中执行一些操作”。如果除了等待之外的所有动作都在一组比无用更糟糕的动作中,那么“永远等待”的秒数 N 不会明显不同于......
    • ..."等待 N+1 秒,然后执行一些其他操作",因此无法观察到除了等待之外的可容忍操作集为空的事实。另一方面,如果一段代码从一组可能的动作中删除了一些无法容忍的动作,并且其中一个动作无论如何都被执行,那么这应该被认为是可观察的。不幸的是,C 和 C++ 语言规则以一种奇怪的方式使用“假设”这个词,这与我能识别的任何其他逻辑领域或人类努力不同。
    • @FamousJameis 好的,但是 Clang 不只是删除循环 - 它会在之后静态分析所有内容为无法访问并发出无效指令。如果它只是“删除”循环,那将不是你所期望的。
    猜你喜欢
    • 2012-05-05
    • 2011-04-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-04-12
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多