关于循环变量优化的标准合规行为是什么?



在下面的循环中

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 < 10i的内存读取,并始终读取加载的第一个值,那么这种行为是否符合标准?我想知道C++语言的哪一部分描述了这种情况下的行为。

编辑:在与@Rakete1111讨论后,我希望专家阐明以下语言及其含义:

对符合性实现的最低要求是:

(7.1) — 严格评估通过易失性gl值进行的访问 根据抽象机器的规则。

(7.2) — 在程序终止时,写入文件的所有数据应 与执行程序的可能结果之一相同 根据抽象语义会产生。

(7.3) — 交互式设备的输入和输出动态应 以这样的方式发生,提示输出实际上是 在程序等待输入之前交付。什么构成 交互式设备是实现定义的。

这些统称为 程序。[注:摘要和摘要之间更严格的对应关系 实际语义可以由每个实现定义。—尾注 ]

我解释它的方式,这允许优化i的读取。我在这里错过了什么?

C++ 标准描述了一个抽象的虚拟机,如果源代码根据其规则有效,则以它描述的方式运行程序。 实际的C++编译器是实现环境的代码,该环境尽可能与标准中描述的虚拟机匹配。 虽然该标准很少提及特定的优化,但它确实需要编译器实现:

4.6 第5段:

执行格式良好的程序的符合要求的实现应产生与 具有相同程序的抽象机器的相应实例的可能执行之一 和相同的输入。但是,如果任何此类执行包含未定义的操作,则此 国际 标准对使用该输入执行该程序的实现没有要求(甚至没有要求 关于第一个未定义操作之前的操作)。

在您的情况下,如果编译器看不到 function_that_increments_i() 定义,那么它无法证明不会修改 i,并且必须悲观/防御性地假设它修改了,并且不应用具有此先决条件的修改。

但是,如果 function_that_increments_i() 的定义是可见的,可以内联,并且它不会将对 i 的引用传递给它无法跟踪的其他函数,那么它仍然可以应用一系列内联/减少优化。 一旦删除了对 function_that_increments_i() 的调用,就有可能消除(或更改)i 的表示形式。

重要的是它必须保持相同的行为。

好的,如果您愿意,让我们使用标准,并证明一致的编译器供应商将在每次迭代中正确评估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
{ }
}

—结束示例 ]

在这里,我们只在告诉我们条件运算符if的捐赠部分是full-expression的例子中穿插

。接下来我们想知道全 ex[ressions是如何排序的:

与全表达式相关的每个值计算和副作用在与

要评估的下一个全表达式相关的每个值计算和副作用之前进行排序。

很酷,所以我们看到所有可能来自function_that_increments_i()的副作用都必须在下次评估条件之前进行评估。

好吧,但是为什么一个符合标准的编译器的供应商必须关心抽象机器希望你的程序如何表现呢?为此,我们从标准中摘录了另一段内容:

本国际标准对符合性实施的结构没有要求。 特别是,它们不需要复制或模拟抽象机器的结构。相反,符合 需要实现来模拟(仅)抽象机器的可观察行为,如前所述 下面。

证明到此结束。

我还建议您阅读有关假设规则的信息。

不,它不会,因为你的循环在 10 次迭代后结束,但如果编译器优化了i,那么你的循环是无限的。这违反了非正式的假设规则:只要您没有注意到发生了这样的优化,就允许进行任何优化。

基本上,如果您有:

// sum is long = 0, n is int something
for (int i = 0; i <= n; ++i)
sum += i;

然后允许编译器将其重写为:

sum = n + n(n-1)/2

因为你无法注意到差异,因为两者做着完全相同的事情,基本上"好像"。

该标准本质上要求执行程序来执行"与抽象机器的逐步执行相同的事情"。每个操作的抽象机器的执行是由标准的各种语义规定定义的,重要的是要了解抽象机器没有优化,没有捷径,并且勤奋地遵守程序中编写的每个操作。在抽象机器的语义中注意允许它不做程序编写的内容(例如,读取变量的值)。

如果修改无法更改没有未定义行为的格式良好的程序的可观察行为,则"as-if"规则允许优化(或通常的程序修改)。例如,如果计算了一个值,但从未以可能可见的方式使用(例如,通过写出),则可以消除计算。但要进行这种优化,编译器必须能够证明该值永远不会被使用。它不能只是猜测它可能不会被使用。

从本质上讲,几乎不可能消除对非内联函数的调用,例如您问题中的函数。函数的签名不指示函数是否具有某些可观察的行为(例如写入 stdout),因此必须调用它。

类似地,如果一个变量没有被声明volatile,只有当编译器能够证明该值不会被使用,或者该值可以在不读取变量的情况下被推导出来时,才能消除对该变量的读取。一种可能性是,编译器可以证明没有未定义行为的格式良好的程序不能修改变量的值,从而允许使用先前读取的值。 (如果变量被声明为const,这将更容易证明。

对于地址被占用的非常量变量,编译器将很难证明该值没有更改,并且如果执行包括调用其定义不可见的外部函数,则无法证明。在没有这样的证明的情况下,编译器有义务符合抽象机器的行为。

最新更新