【问题标题】:What is the standard compliant behavior regarding optimization of loop variable?关于循环变量优化的标准兼容行为是什么?
【发布时间】:2018-02-03 14:07:02
【问题描述】:

在下面的循环中

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

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

对一致性实现的最低要求是:

(7.1) — 通过 volatile glvalues 的访问被严格评估 根据抽象机的规则。

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

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

这些统称为可观察的行为 程序。 [注:abstract 和 实际语义可以由每个实现来定义。 ——尾注]

按照我的解释,这可以优化对i 的读取。我在这里错过了什么?

【问题讨论】:

  • 但您需要以某种方式将i 传递给函数。我认为,你需要提供一个具体的例子。
  • @Yola 看看第一条评论,上面写着i 的地址已被传递给程序的其他部分。该函数通过该指针的副本访问i
  • other part 可能在翻译单元中为 function_that_increments_i 或在另一个单元中,这可能很重要。
  • 如果您不向其中传递任何参数,您将如何从function_that_increments_i 访问i?通过全局变量?我认为编译器可以检测到它。
  • @Yola 是的,编译器可以检测到这一点,但标准的哪一部分强制执行它?

标签: c++ memory optimization compiler-construction


【解决方案1】:

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

4.6 第 5 段:

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

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

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

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

【讨论】:

  • 可能是编译器看不到定义,如果能证明函数无权访问变量,还是可以推断出不能更新变量?跨度>
  • 如果i是一个局部变量并且没有创建指向它的指针(例如,任何地方都没有&amp;i),那么可以证明函数之外的任何东西都不可能(合法,以定义的方式)访问或修改i,而不查看函数本身之外的任何内容。当然我不知道function_that_increments_i()(没有任何参数)应该如何增加i(除了奇怪/未定义的堆栈粉碎恶作剧)。
  • @Chris Uzdavinis 您引用的部分是通用语言,没有描述我问题中的行为指针。
  • @Brendan 根据代码中的第一条注释,i 的地址已传递给程序的另一部分。我试图准确地找到标准中限制编译器优化读取i(如果其地址已在其他地方传递)的部分。
  • 我不确定你会在标准中找到那种语言。它定义了程序的行为应该是什么,并且大部分情况下将完成的“如何”留给编译器实现。
【解决方案2】:

好的,如果您愿意,让我们使用标准并证明符合标准的编译器供应商将在每次迭代中正确评估 i。首先让我们看看标准的目的:

本国际标准中的语义描述定义了一个参数化的非确定性抽象机。

接下来让我们研究一下这个抽象机器希望你的程序如何表现。让我们看看它对for-loop 语句的定义:

for 语句

for ( for-init-statement conditionopt; expressionopt) statement

等价于

{
    for-init-statement
    while ( condition ) {
        statement
        expression ;
    }
}

这将我们发送到while 声明:

形式的while语句

while (T t = x) statement

等价于

label:
{ // start of condition scope
    T t = x;
    if (t) {
        statement
        goto label;
    }
}

所以i &lt; 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 的dondition 部分是full-expression

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

与完整表达式关联的每个值计算和副作用都在与要评估的下一个完整表达式关联的每个值计算和副作用之前排序。

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

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

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

证明到此结束。

我还建议您阅读as-if规则。

【讨论】:

    【解决方案3】:

    不,它不会,因为您的循环在 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
    

    因为你看不出区别,因为两者做的事情完全相同,基本上“好像”。

    【讨论】:

    • 但是i 不是volatile,因此从i 读取不会构成副作用。因此,它不会影响程序的可观察状态。这是我的理解,欢迎指正。
    • 编译器可以展开循环。
    • @vvvcvvcv 这在技术上是正确的。如果“// Do some more work”只是一个注释,如果它实际上并没有做任何工作,并且如果function_that_increments_i() 只是增加i,而i 在循环结束后未被使用,那么当然整个事情都可以优化掉。但是假设答案似乎是不合理的,假设答案应该假设是不合理的。合理的假设是“// Do some more work”包含确实有副作用的代码,然后必须执行确切的执行次数。
    • @hvd 我并不是说编译器会优化掉循环,我问的是如果编译器优化掉表达式 i &lt; 10 中的 i 的内存读取会怎样。
    • @vvvcvvcv 让我简化一下,然后:假设您在问题中没有歪曲任何内容,那么您关于它不会影响程序的可观察状态的评论是不正确的。
    【解决方案4】:

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

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

    本质上,几乎不可能消除对非内联函数的调用,例如您问题中的函数。函数的签名并没有说明函数是否有一些可观察到的行为(比如写入stdout),所以必须调用它。

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

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

    【讨论】:

      猜你喜欢
      • 2020-09-09
      • 2010-09-19
      • 1970-01-01
      • 1970-01-01
      • 2018-11-01
      • 2013-06-29
      • 2018-07-12
      相关资源
      最近更新 更多