编译器是否允许消除无限循环?
可以优化编译器删除无限循环,这不会改变任何数据,如
while(1) /* noop */;
从分析编译器可以派生的数据stream图来看,这样的循环是“死代码”,没有任何副作用。
删除C90 / C99标准禁止的无限循环?
C90或C99标准是否允许编译器删除这样的循环?
更新:“微软C版本6.0本质上做了这种优化。”,见caf链接。
label: goto label; return 0;
将被转化为
return 0;
C11在C11标准部分草案中澄清了这个问题的答案6.8.5
迭代声明增加了以下段落:
一个迭代语句,其控制expression式不是一个常量expression式,不执行input/输出操作,不访问易失性对象,在其主体中不执行同步或primefaces操作,控制expression式或(在for声明)其expression式-3,可以由实现来承担终止。 157)
脚注157
说:
这是为了允许编译器转换,例如,即使在无法certificate终止的情况下也可以清除空循环。
所以你的具体例子是:
while(1) /* noop */;
控制expression式是一个常量expression式,因此是不公平的优化游戏。
作为UB的无限循环
那么为什么编译器允许优化无限循环,除了上面提供的例外,Hans Boehm提供了一个无限循环的未定义行为的基本原理: N1528:为什么对于无限循环的未定义行为? ,下面的引用给出了涉及到的问题的一个好的感觉:
正如N1509正确指出的那样,当前的草案在6.8.5p6中给出了无限循环的无限循环。 这样做的一个主要问题是,它允许代码跨越可能不终止的循环。 例如,假设我们有以下循环,其中count和count2是全局variables(或者已经取得了它们的地址),而p是一个局部variables,其地址未被采用:
for (p = q; p != 0; p = p -> next) { ++count; } for (p = q; p != 0; p = p -> next) { ++count2; }
可以将这两个循环合并并replace为以下循环吗?
for (p = q; p != 0; p = p -> next) { ++count; ++count2; }
如果没有在6.8.5p6中对无限循环的特殊configuration,这将是不允许的:如果第一个循环没有终止,因为q指向一个循环列表,那么原始logging不会写入到count2。 因此它可以与访问或更新count2的另一个线程并行运行。 尽pipe存在无限循环,但对于访问count2的转换版本来说,这已经不再安全。 因此,转换可能引入数据竞赛。
在这种情况下,编译器不太可能certificate循环终止; 它必须明白,q指向一个非循环列表,我相信这是超出了大多数主stream编译器的能力,并且通常不可能没有整个程序信息。
C99
既然C99没有这样做,我们可以看看5.1.2.3
节中的as-if规则 ,它基本上说编译器只需要模拟一个程序的可观察行为,其要求如下:
符合实现的最低要求是:
- 在顺序点上,易失性对象是稳定的,因为之前的访问已经完成,后续的访问还没有发生。
- 在程序结束时,写入文件的所有数据应该与根据抽象语义执行程序所产生的结果相同。
- 交互设备的input和输出dynamic应按照7.19.3的规定进行。 这些要求的意图是尽可能快地出现无缓冲或线路缓冲的输出,以确保提示消息实际上出现在等待input的程序之前。
严格阅读这似乎允许一个实现优化一个无限循环。 我们当然可以想出优化一个无限循环会导致可观察行为发生变化的场景:
while(1) ; printf( "hello world\n" ) ;
许多人会认为,实现一个过程的终止也是可观察的行为,这个位置是在C编译器中驳斥的费马大定理 :
编译器在实现C程序方面有很大的自由度,但是它的输出必须具有与标准中描述的“C抽象机器”所解释的相同的外部可见行为。 许多知识渊博的人(包括我)都认为,程序的终止行为是不能改变的。 很显然,一些编译器的作者不同意,或者不相信这很重要。 合理的人对解释的不同意见似乎表明C标准是有缺陷的。
更新
我不知何故错过了上述文章“ 编译器和终止再访 ”的后续内容,其中说明5.1.2.3
节如下:
第二个要求是棘手的。 如果是在说抽象机器上运行的程序的终止,那么因为我们的程序没有终止,所以它是空的。 如果它正在讨论编译器生成的实际程序的终止,那么C实现就会有问题,因为写入文件(stdout是一个文件)的数据不同于抽象机写入的数据。 (这个读数是由汉斯·伯姆引起的,我没有把这个微妙的东西弄清楚。)
人们也可以做出一个较弱的观点,即需要在C11中创build一个允许清除空循环的暗示意味着这不是以前的允许优化。
这是否也适用于无限循环?
我相信其意图是,这也适用于无限转到循环。 在C ++中,显然是这样的,因为1.10
节[intro.multithread]说:
实现可能会假定任何线程最终都会执行以下操作之一
- 终止,
- 拨打一个库的I / Ofunction,
- 访问或修改易失性对象,或
- 执行同步操作或primefaces操作。
然后在N1528
expression的N1528
是C和C ++标准是一致的:
由于编译器后端通常是在C和C ++编译器之间共享的,所以WG14和WG21对于采用哪种解决scheme似乎是最重要的。 替代scheme将由后端对这两种语言进行特殊处理,或者在处理C代码时禁用优化。 两者都不可取。
最后说:
WG21正在考虑改进措辞,使治疗一致。 希望WG14能跟踪任何由此产生的变化。
目前C11标准并没有包含5.1.2.4
multithreading执行和数据 N1528
类似的措辞,但是考虑到N1528
,假设编译器将把无限goto循环视为C和C ++中未定义的行为似乎是明智的。
请注意, 这里也请参阅美国的评论38和N3196 ,这个改变的论文是从那里申请的。
无法普遍检测到无限循环:请参阅暂停问题 。 所以最好的编译器可以做一个体面的猜测 – 例如在OP中提到的明显情况。
但为什么这是可取的? 我可以看到发出一个警告,并仍然允许的行为,但删除循环不是一个“优化” – 它改变了程序的行为!
循环不是死代码,它基本上阻止了程序的到达。 这不是如果循环被删除会发生什么,所以编译器不能删除循环。
它可能会取代它与平台相关的空闲指令,以通知处理器线程是不会做任何事情。
编译器可以做的是删除循环后的任何代码,因为它是无法访问的,永远不会被执行。
在comp.lang.c
之前(例如这里 )已经讨论过很多次了,据我所知,没有任何共识的结果。
在编写守护进程时,它们是必需的。 你为什么要叫他们死代码?
我相信,新的标准明确规定,如果一段代码不访问任何易变的variables,执行I / O等等任何不依赖于第一部分计算出的任何其他代码,可以在它之前任意sorting。 如果一个无限循环不执行任何I / O,也不计算稍后在程序中使用的任何值,编译器可能只是推迟循环的执行,直到其他所有事情完成。