如何制作不会被优化的无限空循环?


131

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");
}

在我的机器上,此打印输出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");
}

...打印begin出来的unreachable是无穷循环,仿佛永无止境。

如何在启用优化的情况下让Clang输出正确的,无内存访问的无限循环?


3
评论不作进一步讨论;此对话已转移至聊天
巴尔加夫(Bhargav Rao)

2
没有没有副作用的便携式解决方案。如果您不希望访问内存,则最大的希望就是注册volatile unsigned char。但是寄存器在C ++ 17中消失了。
Scott M

25
也许这不在问题的范围内,但是我很好奇您为什么要这样做。当然,还有其他方法可以完成您的实际任务。还是这只是学术性质?
Cruncher

1
@Cruncher:任何特定尝试运行程序的结果都可能是有用的,基本上是无用的,或者比无用的要严重得多。导致程序陷入无限循环的执行可能没有用,但比编译器可能会替代的其他行为更可取。
超级猫

6
@Cruncher:因为代码可能在没有概念的独立上下文中运行exit(),并且因为代码可能已经发现无法保证继续执行的效果不会比没有用的效果更糟的情况。跳转到自我循环是处理此类情况的一种很糟糕的方法,但它仍然可能是处理不良情况的最佳方法。
超级猫

Answers:


77

C11标准是6.8.5 / 6:

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

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

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

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

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

据我所知,循环后“无法访问的代码”发生了什么,尚不清楚。但是,c确实确实表现得很奇怪。将机器代码与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

lang 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)并停在那里,仅打印“ begin”(假定\ n刷新stdout)。这发生在gcc中。

如果只是删除了循环,则应打印“ begin”两次,然后打印“ unreachable”。但是,在clang(godbolt)上,它先打印“ begin” 1次,然后“ unreachable”,然后返回退出代码0。无论您如何输入,这都是很错误的。

我在这里找不到主张未定义行为的情况,因此我认为这是clang中的错误。无论如何,这种行为会使clang 100%对于嵌入式系统之类的程序毫无用处,在嵌入式系统中,您仅必须能够依靠永久循环挂起程序(在等待看门狗时)。


15
我不同意“这是一个非常明确的常量表达式,因此实现可能不认为它终止”。这确实进入了挑剔的语言律师界,但是6.8.5/6采取if(these)的形式,那么您可以假设(this)。这并不意味着(如果没有)您可能不会假设(这)。它仅用于满足条件的规范,而不满足未满足条件的规范,您可以根据标准执行任何操作。如果没有可观察到的东西……
kabanus

7
@kabanus引用的部分是特例。如果不是(特殊情况),请照常评估和排序代码。如果您继续阅读同一章,则除了引用的特殊情况外,将按照为每个迭代语句指定的方式(按语义指定的方式)对控制表达式进行求值。它遵循与评估任何值计算相同的规则,该计算是顺序确定的。
隆丁

2
我同意,但你不会surpised,在int z=3; int y=2; int x=1; printf("%d %d\n", x, z);没有2在组装,所以在空无用感x后未分配y,但后z因优化。因此,从您的最后一句话开始,我们将遵循常规规则,假设停顿了一下(因为我们没有受到任何更好的约束),并留在了最终的“无法到达”的字样中。现在,我们优化出该无用的语句(因为我们不知道更好)。
kabanus

2
@MSalters我的评论之一已删除,但感谢您的输入-我同意。我的评论是,我认为这是辩论的核心- 就允许我们优化哪些语义(即使其逻辑仍然存在)而言,这while(1);int y = 2;声明相同 。从n1528开始,我给人的印象是它们可能是相同的,但是由于人们比我更有经验,他们在争论另一种方法,而且显然这是一个官方的错误,因此超出了有关标准措辞是否明确的哲学辩论。 ,则该论据毫无意义。
kabanus

2
“由于“永远”循环是常见的编程构造,因此这种实现将无可避免地被破坏。” —我理解这种观点,但是该论点存在缺陷,因为它可以同样地应用于C ++,但是优化了这种循环的C ++编译器不会被破坏,而是会保持一致。
康拉德·鲁道夫

52

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

最简单的解决方案:

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

Godbolt链接


21
但是,这并不能解释为什么clang在起作用。
隆丁

4
只需说“这是clang中的错误”就足够了。不过,在我大喊“臭虫”之前,我想先在这里尝试一些方法。
隆丁

3
@Lundin我不知道这是否是错误。标准是不是在这种情况下,技术上精确
P__J__

4
幸运的是,GCC是开源的,我可以编写一个编译器来优化您的示例。对于您现在和将来提出的任何示例,我都可以这样做。
Thomas Weller

3
@ThomasWeller:GCC开发人员不会接受优化此循环的补丁;它会违反记录的=保证的行为。参见我之前的评论:asm("")是隐式的asm volatile("");,因此asm语句必须与抽象机gcc.gnu.org/onlinedocs/gcc/Basic-Asm.html中的运行次数相同。(请注意,这不是安全的副作用包括任何存储器或寄存器,你需要一个扩展ASM "memory"撞,如果你想读或写的存储器,从C.基本ASM你曾经访问只对类的东东安全asm("mfence")cli)。
Peter Cordes

50

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

例如,Rust编程语言还允许无限循环,并使用LLVM作为后端,并且它具有相同的问题。

在短期内,LLVM似乎将继续假设“必须终止所有没有副作用的循环”。对于允许无限循环的任何语言,LLVM都希望前端将llvm.sideeffect操作码插入此类循环中。这正是Rust计划要做的,因此Clang(在编译C代码时)可能也必须这样做。


5
没有什么比十年前的bug的气味更令人回味了。
伊恩·坎普

4
@IanKemp:对于他们来说,现在要修复此错误,需要确认他们已经花了十年的时间修复了该错误。最好抱有希望标准会改变以证明其行为合理的希望。当然,即使标准确实发生了变化,但除非有人认为标准的变化是标准早先的行为授权是应当追溯纠正的缺陷,否则这仍然不能证明他们的行为合理。
超级猫

4
从某种意义上说,它已被“修复”,LLVM sideeffect在2017年添加了op,并期望前端根据自己的判断将op插入循环。LLVM必须为循环选择一些默认值,而恰巧是有意或无意地选择了一种符合C ++行为的循环。当然,还有一些优化工作要做,例如将连续的sideeffect操作合并为一个。(这就是阻止Rust前端使用它的原因。)因此,在此基础上,该bug出现在前端(that)中,没有在循环中插入op。
Arnavion

@Arnavion:是否有任何方法可以指示除非使用或直到使用结果才可以推迟操作,但是如果数据导致程序无休止地循环,那么尝试越过数据依赖关系会使程序变得比没用更糟?必须添加伪造的副作用,这会阻止以前有用的优化,以防止优化程序使程序变得比没用的更糟,这听起来不像是提高效率的秘诀。
超级猫

该讨论可能属于LLVM / clang邮件列表。FWIW添加了op的LLVM提交也确实教导了一些优化途径。同样,Rust尝试将sideeffectops 插入每个函数的开头,但没有看到任何运行时性能下降。唯一的问题是编译时间回归,这显然是由于缺乏连续操作的融合,就像我在之前的评论中提到的那样。
Arnavion

32

这是一个Clang错误

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

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


要回答标题问题:如何制作不会被优化的无限空循环??-
创建die()宏而不是函数,以解决Clang 3.9及更高版本中的此错误。(早期的Clang版本要么保留循环,要么call使用无限循环将其发送到函数的非内联版本。)即使该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中不是UB。is
(1); C? UB中的未定义行为使编译器基本上可以发出任何东西对于肯定会遇到UB的执行路径上的代码,asm循环中的语句将避免使用UB for C ++。但实际上,Clang编译为C ++不会删除常量表达式无限空循环,除非在内联时与编译为C。)


手动内联while(1);更改Clang的编译方式:asm中存在无限循环。 这就是我们从规则律师POV所期望的。

#include <stdio.h>
int main() {
    printf("begin\n");
    while(1);
    //infloop_nonconst(1);
    //infloop();
    printf("unreachable\n");
}

在Godbolt编译器资源管理器上,将Clang 9.0 -O3编译为C(-xc)用于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"

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

有效的选择是

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

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


脚注1:

记录下来,我同意@Lundin的回答,该回答引用了标准,以证明C11不允许假设常量表达式无限循环的终止,即使它们为空(没有I / O,volatile,同步或其他)可见的副作用)。

这是一组条件,可以将循环编译为正常CPU 的空asm循环。(即使源代码中的主体不为空,在循环运行时,没有数据争用UB,其他线程或信号处理程序也看不到变量的赋值。因此,如果需要的话,一致的实现可以删除此类循环主体然后留下了是否可以删除循环本身的问题。ISOC11明确表示否。)

鉴于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开始具有低功耗空闲状态之前节省大量电量。)


1
出于好奇,有人在为嵌入式系统使用clang吗?我从未见过它,我只与嵌入式系统一起工作。gcc仅“最近”(十年前)才进入嵌入式市场,我对此表示怀疑,最好是在优化程度较低的情况下始终使用-ffreestanding -fno-strict-aliasing。它与ARM以及旧版AVR都可以正常工作。
隆丁

1
@Lundin:IDK关于嵌入式,但是是的,人们确实使用clang(至少有时是Linux)构建内核。大概还有MacOS的Darwin。
彼得·科德斯

2
bugs.llvm.org/show_bug.cgi?id=965这个错误看起来很相关,但是我不确定这是我们在这里看到的。
bracco23

1
@lundin-我很确定我们在整个90年代都使用GCC(以及许多其他工具包)进行嵌入式工作,并使用了VOS和PSOS等RTOS。我不明白您为什么说GCC最近才进入嵌入式市场。
Jeff Learman

1
@JeffLearman最近成为主流了吗?无论如何,gcc严格混叠的惨败只是在C99引入之后才发生,并且它的较新版本在遇到严格混叠的违反时也似乎不再流行。不过,无论何时使用它,我仍然持怀疑态度。至于clang,就永恒循环而言,最新版本显然已被完全破坏,因此它不能用于嵌入式系统。
伦丁

14

仅作记录,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名单whiledo以及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

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


合格的实现可能对执行时间或CPU周期有未记录的转换限制,如果超出限制,或者如果程序输入不可避免地超出限制,则可能导致任意行为。此类问题属于标准管辖范围之外的实施质量问题。clang的维护者会如此坚持要产生质量差的实现的权利,这似乎很奇怪,但是标准确实允许这样做。
超级猫

2
@supercat感谢您的评论...为什么超出翻译限制会导致翻译阶段失败并拒绝执行?另外:“ 5.1.1.3诊断如果预处理翻译单元或翻译单元包含违反任何语法规则或约束的条件,则一致的实现应产生……诊断消息”。我看不到执行阶段的错误行为怎么能符合。
jonathanjo

如果必须在构建时就解决所有实现限制,则该标准将完全不可能实施,因为一个人可以编写一个严格合规的程序,该程序需要的堆栈字节数要多于宇宙中的原子数。尚不清楚是否应将运行时限制与“转换限制”混为一谈,但是这种让步显然是必要的,没有其他类别可以使用。
超级猫

1
我是在回应您有关“翻译限制”的评论。当然也有执行限制,我承认我不明白为什么您建议将它们与翻译限制混为一谈,或者为什么您说这是必要的。我只是看不出任何理由nasty: goto nasty可以符合要求,并且在用户或资源枯竭之前,不要旋转CPU。
jonathanjo

1
该标准未提及我可以找到的“执行限制”。诸如函数调用嵌套之类的事情通常由堆栈分配处理,但是将函数调用的深度限制为16的符合标准的实现可以构建每个函数的16个副本,并bar()在内部foo()将to 的调用作为从__1footo __2bar,from __2foo至to 的调用进行处理__3bar。等等,然后从__16footo到__launch_nasal_demons,这将允许静态分配所有自动对象,并将通常的“运行时”限制转换为转换限制。
超级猫

5

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

不执行输入/输出操作,不访问易失对象,并且在其主体,控制表达式中或者(对于语句)其表达式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说:

在抽象机中,所有表达式均按语义指定的方式求值。如果实际实现可以推断出未使用表达式的值并且没有产生所需的副作用(包括由调用函数或访问易失性对象引起的副作用),则无需评估表达式的一部分。

这里提到的是表达式,而不是语句,因此并不是100%令人信服,但它确实允许以下调用:

void loop(void){ loop(); }

int main()
{
    loop();
}

被跳过。有趣的是,clang会跳过它,而gcc不会


“这并没有说明不满足条件会发生什么。” 6.8.5.1确实如此,while语句:“控制表达式的求值发生在循环体每次执行之前。” 而已。这是(常量表达式的)值计算,它属于定义术语评估的抽象机5.1.2.3的规则:“ 对表达式的评估通常包括值计算和副作用的产生。” 并且根据同一章,所有此类评估均按语义指定的顺序进行排序和评估。
隆丁

1
@Lundin因此while(1){},无限的1评估序列与评估是交织在一起的{},但是在标准中哪条规定这些评估需要花费非零时间呢?我猜,gcc行为更有用,因为您不需要涉及内存访问的技巧或语言之外的技巧。但是我不相信该标准禁止使用clang进行此优化。如果要使结果while(1){}不可优化,则应该对此标准进行明确说明,并且应将无限循环列为5.1.2.3p2中可观察到的副作用。
PSkocik

1
我认为这是指定的,如果您将1条件视为值计算。执行时间无关紧要-重要的是while(A){} B;可能没有完全优化,未优化B;和未重新排序B; while(A){}。要引用C11抽象机,请强调一下:“在表达式A和B的求值之间存在序列点意味着:每一个值计算和副作用与A相关联的每一个值计算之前测序和副作用与B相关联 ”。的值A已明确使用(由循环使用)。
伦丁

2
+1即使在我看来“执行无限期地挂起而没有任何输出”在任何有意义的“副作用”定义中都是“副作用”,并且在真空中不仅超出了标准也有用,但这有助于解释对某人有意义的心态。
mtraceur

1
“优化无限循环”附近“它”是指标准还是编译器尚不完全清楚-可能是改写吗?给定“尽管它应该应该”而不是“虽然它应该不应该”,但这可能是“它”所指的标准。
Peter Mortensen

2

我已经确信这只是一个普通的老错误。由于先前的某些原因,我将我的测试留在下面,尤其是参考标准委员会中的讨论。


我认为这是未定义的行为(请参见结尾),Clang仅有一个实现。GCC确实按照您的期望工作,仅优化了unreachable打印语句,但退出了循环。当组合内联并确定它可以对循环做什么时,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);
}

做这份工作。它很费力地优化(显然),并留在多余的final中printf。至少程序不会停止。也许毕竟是海湾合作委员会?

附录

在与David讨论之后,我得出的标准不是说“如果条件恒定,则您可能不会认为循环终止”。因此,根据标准,没有可观察到的行为(如标准中所定义),我只为保持一致性而争论-如果编译器由于假定循环终止而正在优化循环,则不应优化以下语句。

eck 如果我没看错的话, n1528会将其作为未定义的行为。特别

这样做的主要问题是它允许代码在可能终止的循环中移动

从这里开始,我认为它只能演变成关于我们想要什么(期望?)而不是允许的讨论。


评论不作进一步讨论;此对话已转移至聊天
巴尔加夫(Bhargav Rao)

关于“普通虫子”:您是说普通的旧虫子”吗?
Peter Mortensen

@PeterMortensen“ ole”也可以。
kabanus

2

看来这是Clang编译器中的错误。如果该die()函数没有任何强制要成为静态函数,请废除static并将其制成inline

#include <stdio.h>

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

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

使用Clang编译器进行编译时,它可以按预期工作,并且可移植。

编译器资源管理器(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
SS安妮

1

以下内容似乎对我有用:

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

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


1
printf如果循环确实永远消失了,则不必生成第二个,因为在这种情况下,第二个printf实际上是无法访问的,因此可以删除。(Clang的错误在于检测不可达性,然后删除循环以使达到不可达代码。)
nneonneo

GCC文档__attribute__ ((optimize(1))),但是clang忽略了它,因为它不受支持: godbolt.org/z/4ba2HMgcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html
彼得·科德斯

0

符合标准的实现可能并且许多实际的方法会对程序执行的时间或执行的指令施加任意限制,并且如果违反了这些限制或在“按条件”规则下以任意方式运行-如果确定不可避免地会受到侵犯。前提是实现可以成功处理至少一个名义上执行N1570 5.2.4.1中列出的所有限制的程序,而不会达到任何翻译限制,限制的存在,记录的程度以及超出这些限制的影响本标准管辖范围之外的所有实施质量问题。

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


-2

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

允许编译器优化标准中未定义的任何行为。这包括执行时间。不需要保留以下事实:如果不进行优化,则循环将花费无数的时间。可以将其更改为更短的运行时间-实际上,这是大多数优化的关键。您的循环已优化。

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


评论不作进一步讨论;此对话已转移至聊天
塞缪尔·柳

4
从您的经验(从您的个人资料)来看,我只能得出结论,这篇文章是出于恶意而写的,只是为了捍卫编译器。您是在认真争论,可以将花费无限时间的内容优化为执行时间的一半。在每个级别上这都是荒谬的,您知道这一点。
烟斗

@pipe:我认为clang和gcc的维护者希望该标准的未来版本将允许其编译器的行为允许,并且那些编译器的维护者将能够假装这种更改只是对长期缺陷的纠正。在标准中。例如,这就是他们对待C89的Common Initial Sequence保证的方式。
超级猫

@SSAnne:嗯...我认为不足以阻止指针相等比较结果中的一些不合理的推断gcc和clang绘制。
超级猫

@supercat还有<s> others </ s>吨。
SS安妮

-2

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

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

1
关键是在启用优化的情况下进行无限循环。
SS安妮

-4

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

因此Clang将其删除。有一些“更好的”方法可以实现预期的行为,从而迫使您更加清楚自己的意图。

while(1); 是baaadd。


6
在许多嵌入式构造中,没有abort()或的概念exit()。如果出现某种情况,其中某个函数确定(可能是由于内存损坏而导致)继续执行会比危险更糟,则嵌入式库的常见默认行为是调用执行的函数while(1);。对于编译器而言,选择替代更多选项可能很有用。用的行为,但是任何无法弄清如何将这种简单构造视为继续执行程序的障碍的编译器作者都无法通过复杂的优化来信任。
超级猫

有没有一种方法可以使您的意图更加明确?优化器在那里可以优化您的程序,删除不执行任何操作的冗余循环就是一种优化。这实际上是数学界的抽象思维与更实用的工程界之间的哲学差异。
著名的贾梅斯

大多数程序都有一组应在可能的情况下执行的有用操作,以及一组在任何情况下都不得执行的比无用的更糟糕的操作。在任何特定情况下,许多程序都有一组可接受的行为,如果无法观察到执行时间,则其中的一个总是“等待一些任意行为,然后从该组中执行一些操作”。如果除了等待以外的所有其他动作都在一组比没用的更糟糕的动作中,那么没有秒数N可以表示“永远等待”与……
超级猫

...“等待N + 1秒,然后执行一些其他操作”,因此无法观察到除等待之外的可容忍操作集为空的事实。另一方面,如果一段代码从一组可能的动作中删除了一些无法忍受的动作,并且其中一个动作被执行无论如何,则应将其视为可观察到的。不幸的是,C和C ++语言规则以奇怪的方式使用了“假设”一词,这与我可以识别的任何其他逻辑或人类努力领域都不一样。
超级猫

1
@FamousJameis好的,但是Clang不仅删除了循环-以后还会静态分析所有内容(如果无法访问)并发出无效指令。如果它只是“消除”了循环,那将不是您所期望的。
nneonneo
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.