在以下循环中
int i = 0;
// pass address of i to some other part of the program
// Do some work
for (; i < 10;)
{
// Do some more work
function_that_increments_i();
}
如果出于某些(奇怪的,理论上的)原因,编译器会优化i
中i < 10
的内存读取并始终读取加载的第一个值,这种行为是否符合标准?我想知道C ++语言的哪一部分描述了这种情况下的行为。
编辑:在与@ Rakete1111讨论后,我希望专家解释以下语言及其含义:
符合实施的最低要求是:
(7.1) - 严格评估通过volatile glvalues的访问 根据抽象机的规则。
(7.2) - 程序终止时,所有写入文件的数据均为 与执行程序的可能结果相同 根据抽象语义会产生。
(7.3) - 交互设备的输入和输出动态 以实际提示输出的方式发生 在程序等待输入之前交付。什么构成了 交互设备是实现定义的。
这些统称为。的可观察行为 程序。 [注:抽象和抽象之间更严格的对应关系 实际语义可以由每个实现定义。 - 后注]
我解释它的方式,这允许优化i
的读取。我在这里缺少什么?
答案 0 :(得分:4)
C ++标准描述了一个抽象虚拟机,如果源代码根据其规则有效,则以其描述的方式运行程序。实际的C ++编译器是实现一个环境的代码,该环境尽可能地匹配标准中描述的虚拟机。虽然该标准很少提到特定的优化,但它确实需要编译器实现:
4.6第5段:
执行格式良好的程序的符合实现应产生与之相同的可观察行为 具有相同程序的抽象机器的相应实例的可能执行之一 和相同的输入。但是,如果任何此类执行包含未定义的操作,则此国际 标准不要求使用该输入执行该程序的实现(甚至不是 关于第一次未定义操作之前的操作。)
在你的情况下,如果function_that_increments_i()的定义是不能用于编译器看到的,然后它&#39; S无法证明它的没有按&#39;吨修改i,并且必须悲观/防御地认为它确实存在,并且不应用具有此先决条件的修改。
但是,如果function_that_increments_i()的定义是可见的,可以内联,并且它没有将对i的引用传递给它无法跟踪的其他函数,那么它仍然可以应用一系列内联/减少优化。一旦删除对function_that_increments_i()的调用,就有可能消除(或改变)i的表示。
重要的是它必须保持相同的行为。
答案 1 :(得分:3)
好的,如果你愿意的话,让我们选择标准,并证明一个合格的编译器供应商会在每次迭代时正确评估i
。首先让我们看看这是标准的目的:
本国际标准中的语义描述定义了参数化的非确定性抽象机器。
接下来让我们研究一下抽象机器如何让你的程序表现。让我们看一下for
的定义 - 循环语句:
for语句
for ( for-init-statement conditionopt; expressionopt) statement
相当于
{ for-init-statement while ( condition ) { statement expression ; } }
这将我们发送到while
声明:
表格的暂时声明
while (T t = x) statement
相当于
label: { // start of condition scope T t = x; if (t) { statement goto label; } }
所以i < 10
是你的条件。
接下来,我们需要证明条件是while
运算符是完整表达式。为此,我们接下来将从标准中摘录:
完整表达式是一个不是子表达式的表达式 另一个表达。 ...... [例子:
struct S { S(int i): I(i) { } int& v() { return I; } private: int I; }; S s1(1); // full-expression is call of S::S(int) S s2 = 2; // full-expression is call of S::S(int) void f() { if (S(3).v()) // full-expression includes lvalue-to-rvalue and // int to bool conversions, performed before // temporary is deleted at end of full-expression { } }
-end example]
这里我们只是在示例中告诉我们条件运算符if
的捐赠部分是full-expression
。
接下来我们想知道 full-ex [ressions 是如何排序的:
在每个值计算和与要评估的下一个完整表达式相关的副作用之前,对与全表达式相关的每个值计算和副作用进行排序。
很酷,所以我们看到所有可能来自function_that_increments_i()
的副作用必须在下次评估条件之前进行评估。
那么,为什么合作编译器的供应商必须关心抽象机器希望程序的行为?为此我们还有另一个标准摘录:
本国际标准对符合实施的结构没有要求。 特别是,它们不需要复制或模拟抽象机器的结构。相反,符合 实现需要模拟(仅)抽象机器的可观察行为,如上所述 下方。
结论证明。
我还建议您阅读 as-if 规则。
答案 2 :(得分:1)
不,它不会,因为你的循环在10次迭代后结束,但如果编译器优化掉i
,那么你的循环是无限的。这违反了非正式的as-if规则:只要您没有注意到发生了这样的优化,就允许进行每项优化。
基本上,如果你有:
// sum is long = 0, n is int something
for (int i = 0; i <= n; ++i)
sum += i;
然后允许编译器将其重写为:
sum = n + n(n-1)/2
因为你不能注意到差异,因为两者做的完全相同,“基本上就像”。
答案 3 :(得分:1)
标准本质上要求执行一个程序来做同样的事情&#34;作为一步一步执行抽象机器会做。每个操作的抽象机器的执行由标准的各种语义规定来定义,重要的是要理解抽象机器没有优化,没有捷径,并且努力服从程序中的每个操作。注意抽象机器的语义允许它不执行编写的程序所做的事情(例如,读取变量的值)。
&#34; as-if&#34;允许进行优化(或者,通常是程序修改)。规则,如果修改不能更改没有未定义行为的格式良好的程序的可观察行为。例如,如果计算一个值但从未以可能可见的方式使用(例如通过写出),则可以消除计算。但要进行优化,编译器必须能够证明该值永远不会被使用。它不能猜测它可能不会被使用。
实质上,几乎不可能消除对非内联函数的调用,例如问题中的函数。函数的签名并不表示函数是否具有某些可观察的行为(例如写入stdout),因此必须调用它。
类似地,如果变量未声明为volatile
,则只有在编译器证明不会使用该值或该值可以使用时,才能消除对该变量的读取在不阅读变量的情况下推断出来。一种可能性是编译器可以证明没有未定义行为的格式良好的程序不能修改变量的值,允许使用先前读取的值。 (如果变量被声明为const
,这将更容易证明。)
如果是非const变量,其地址被采用,编译器将很难证明该值未被更改,并且如果执行包括调用其定义不可见的外部函数,这是不可能证明的。如果没有这样的证明,编译器就必须符合抽象机器的行为。