“Observable行为”和编译器自由地消除/变换件c ++代码

读完这个讨论后,我意识到我几乎完全误解了这个问题:)

由于C ++抽象机器的描述不够严谨(例如,与JVM规范进行比较),并且如果不可能得到精确的答案,我宁愿要获得有关规则的非正式说明,即合理的“良好”(非恶意的)应该遵循。

标准的第1.9部分关于执行自由的关键概念就是所谓的if规则:

只要结果就好像该要求已被遵守一样,只要可以从该程序的可观察行为中确定,实施就可以自由地忽视本标准的任何要求。

根据标准(我引用n3092),术语“可观察到的行为”是指以下内容:

- 严格按照抽象机器的规则评估对易失性对象的访问。

- 在程序终止时,写入文件的所有数据应与根据抽象语义产生的程序执行结果相同。

- 交互设备的输入和输出动态应该以这样一种方式进行,即在程序等待输入之前实际提供提示输出。 什么构成交互设备是由实现定义的。

因此,大致来说,应该保留易失访问操作和操作系统操作的顺序和操作数; 实现可能会在保存这些不变量的程序中进行任意更改(与抽象c ++机器的某些允许行为相比)

  • 期望非恶意实现能够足够宽地处理io操作(例如,来自用户代码的任何系统调用被视为这种操作)是否合理? (例如,RAII互斥锁不包含挥发物的情况下,RAII互斥锁/解锁不会被编译器丢弃)

  • “行为观察”应该从用户定义的c ++程序层面深入到库/系统调用中有多深? 当然,问题仅仅是关于图书馆的调用,并不打算从用户的角度来看io / volatile访问(例如作为新的/删除操作),但可能(并且通常)访问库/系统实现中的volatile或io。 编译器是否应该从用户的角度对待这种调用(并将这些副作用视为不可观察)或从“库”的角度考虑(并将副作用视为可观察的)?

  • 如果我需要防止某些代码被编译器清除,是不是要问以上所有问题并简单地添加(可能是假的)易失性访问操作(将易失性方法所需的操作包装起来,并在我的易失性实例上调用它们自己的类)在任何情况下似乎可疑?

  • 或者我完全错了,除了标准明确提到的情况外,编译器不允许删除任何c ++代码(作为复制消除)


  • 重要的是,编译器必须能够证明代码在删除之前没有副作用(或者确定它具有哪些副作用并用一些等效代码代替它)。 一般来说,由于独立的编译模型,这意味着编译器在某种程度上受限于哪些库调用具有可观察行为并且可以被消除。

    至于它的深度,它取决于图书馆的实施。 在gcc中,C标准库使用编译器属性来通知编译器潜在的副作用(或不存在副作用)。 例如, strlen标记为一个纯属性,允许编译器转换此代码:

    char p[] = "Hi theren";
    for ( int i = 0; i < strlen(p); ++i ) std::cout << p[i];
    

    char * p = get_string();
    int __length = strlen(p);
    for ( int i = 0; i < __length; ++i ) std::cout << p[i];
    

    但是如果没有纯属性,编译器无法知道函数是否有副作用(除非它是内联的,并且可以在函数内部看到),并且不能执行上述优化。

    也就是说,一般来说,编译器不会删除代码,除非它能证明它没有副作用,即不会影响程序的结果。 请注意,这不仅与volatile和io有关,因为任何变量更改都可能在以后出现可观察的行为。

    至于问题3,如果程序的行为与代码存在一样(复制elision是一个例外),那么编译器只会删除您的代码,因此您甚至不应该关心编译器是否删除它。 关于问题4,as-if规则的含义是:如果编译器产生的隐式重构的结果产生相同的结果,则可以自由地执行更改。 考虑:

    unsigned int fact = 1;
    for ( unsigned int i = 1; i < 5; ++i ) fact *= i;
    

    编译器可以自由地用以下代码替换该代码

    unsigned int fact = 120; // I think the math is correct... imagine it is
    

    循环已经结束,但行为是一样的:每个循环交互不会影响程序的结果,并且该变量在循环结束时具有正确的值,即,如果稍后在某些可观察的操作中使用该值,则结果将是 - 如果循环已经执行。

    不要过分担心可观察行为和as-if规则的含义,它们基本上意味着编译器必须产生您在代码中编写的输出,即使它可以通过不同的路径自由获取结果。

    编辑

    @Konrad在strlen的最初例子中提出了一个很好的观点:编译器如何知道strlen调用可以被忽略? 答案是,在最初的例子中,它不能,因此它不能避开呼叫。 没有任何事情告诉编译器, get_string()函数返回的指针并不指向其他地方正在修改的内存。 我已更正示例以使用本地数组。

    在修改的例子中,数组是本地的,编译器可以验证没有其他指向同一内存的指针。 strlen需要一个const指针,所以它承诺不修改包含的内存,并且该函数是纯的,所以它承诺不会修改任何其他状态。 该数组在循环结构中没有被修改,并且收集所有这些信息,编译器可以确定一次调用strlen就足够了。 如果没有纯粹的说明符,编译器无法知道strlen的结果在不同的调用中是否会有所不同,因此必须调用它。


    由标准定义的抽象机器将在给定具体输入的情况下生成一组特定输出中的一个。 一般来说,所有可以保证的是对于那个特定的输入,编译后的代码将产生一个可能的特定输出。 然而,魔鬼在细节中,需要牢记一些要点。

    其中最重要的可能是如果程序有未定义的行为,编译器可以做任何事情。 所有投注都关闭。 编译器可以并且确实使用潜在的未定义行为来优化:例如,如果代码包含类似*p = (*q) ++ ,编译器可以得出结论pq不是同一个变量的别名。

    未指定的行为可能具有相似的效果:实际行为可能取决于优化级别。 所需要的只是实际输出对应于抽象机器可能的输出之一。

    至于volatile ,在stadnard不说,访问volatile对象是可观察的行为,但它留下的“访问”的含义取决于具体实现。 在实践中,这些日子你不可能真正重视volatile 。 实际对挥发性物体的访问可能会以与其在程序中出现的顺序不同的顺序显示给外部观察者。 (这可以说至少是违反了标准的意图,但是,这是大多数现代编译器在现代架构上运行的实际情况。)

    大多数实现将所有系统调用视为“IO”。 关于互斥锁,当然:就C ++ 03而言,只要你启动第二个线程,就会有未定义的行为(从C ++的角度来看,Posix或Windows确实定义了它),并且在C ++ 11中,同步主体是语言的一部分,并约束可能的输出集合。 (当然,如果编译器可以证明它们不是必需的,那么编译器可以消除同步。)

    newdelete操作符是特殊情况。 它们可以由用户定义的版本替换,而这些用户定义的版本可能明显具有可观察到的行为。 如果编译器有一些知道它们没有被替换的手段,那么编译器只能删除它们,而替换没有可观察到的行为。 在大多数系统中,替换是在编译器完成其工作后的链接时定义的,因此不允许更改。

    关于你的第三个问题:我认为你从错误的角度来看它。 编译器不会“消除”代码,程序中的特定语句不会绑定到特定的代码块。 你的程序(完整的程序)定义了一个特定的语义,编译器必须做一些产生具有这些语义的可执行程序的东西。 对于编译器编写者来说,最明显的解决方案是分别采用每个语句并为其生成代码,但这是编译器作者的观点,而不是你自己的观点。 你把源代码放进去,并获得一个可执行文件; 但很多陈述并不导致任何代码,即使对于那些代码,也不一定是一对一的关系。 从这个意义上说,“防止某些代码消除”的想法没有意义:您的程序具有标准指定的语义,并且您可以请求的所有内容(以及您应该感兴趣的所有内容)是最终的可执行文件具有这些语义。 (你的第四点是类似的:编译器不会“删除”任何代码。)


    我不能说编译器应该做什么,但这是一些编译器实际做的

    #include <array>
    int main()
    {
        std::array<int, 5> a;
        for(size_t p = 0; p<5; ++p)
            a[p] = 2*p;
    }
    

    使用gcc 4.5.2的汇编输出:

    main:
         xorl    %eax, %eax
         ret
    

    用矢量替换数组显示新/删除不受排除:

    #include <vector>
    int main()
    {
        std::vector<int> a(5);
        for(size_t p = 0; p<5; ++p)
            a[p] = 2*p;
    }
    

    使用gcc 4.5.2的汇编输出:

    main:
        subq    $8, %rsp
        movl    $20, %edi
        call    _Znwm          # operator new(unsigned long)
        movl    $0, (%rax)
        movl    $2, 4(%rax)
        movq    %rax, %rdi
        movl    $4, 8(%rax)
        movl    $6, 12(%rax)
        movl    $8, 16(%rax)
        call    _ZdlPv         # operator delete(void*)
        xorl    %eax, %eax
        addq    $8, %rsp
        ret
    

    我最好的猜测是,如果编译器不能使用函数调用的实现,则必须将其视为可能具有可观察的副作用。

    链接地址: http://www.djcxy.com/p/79917.html

    上一篇: "Observable behaviour" and compiler freedom to eliminate/transform pieces c++ code

    下一篇: Compiler: What if condition is always true / false