看完这个讨论后,我意识到我几乎完全误解了这件事:)
由于C++抽象机器的描述不够严格(例如,与JVM规范进行比较),如果无法得到准确的答案,我宁愿希望得到关于合理"好"的规则的非正式澄清(非恶意) )实施应遵循.
标准中关于实现自由的第1.9部分的关键概念被称为as-if规则:
只要可以从程序的可观察行为中确定,结果就好像符合要求一样,实施可以自由地忽略本标准的任何要求.
根据标准(I引用n3092),术语"可观察行为"表示以下内容:
- 严格根据抽象机的规则来评估对易失性对象的访问.
- 在程序终止时,写入文件的所有数据应与根据抽象语义产生的程序执行的可能结果之一相同.
- 交互设备的输入和输出动态应以在程序等待输入之前提示输出实际传送的方式进行.构成交互设备的是实现定义的.
因此,粗略地说,应保留易失性访问操作和io操作的顺序和操作数; 实现可以在保存这些不变量的程序中进行任意更改(与抽象c ++机器的某些允许行为相比)
期望非恶意实现对操作进行足够广泛的处理是否合理(例如,来自用户代码的任何系统调用都被视为此类操作)?(例如,如果RAII包装器不包含挥发物,则编译器不会丢弃RAII互斥锁定/解锁)
"行为观察"应该从用户定义的c ++程序级别深入到库/系统调用中有多深?现在的问题是,当然,只有该库的调用不打算有从用户角度来看IO /挥发性接入(例如,作为新/删除操作),而且可能(通常如此)访问挥发,或在图书馆/系统IO 实现.应该从用户的角度来看,编译器将这类电话(并考虑副作用,如没有可观察到的),或从"库"的观点(以及考虑的副作用观察到的)?
如果我需要通过编译器,以防止一些代码消除,这是一个好的做法,是不会问上面的所有问题,并简单地增加(可能是假的)挥发性存取操作(换行需要挥发的方法的行动,并呼吁他们对挥发性情况我在任何情况下似乎可疑?
或者我完全错了,除了标准明确提到的情况(作为副本消除)之外,编译器不允许删除任何c ++代码
Dav*_*eas 10
重要的是,编译器必须能够证明代码没有副作用才能将其删除(或确定它具有哪些副作用并用一些等效代码替换它).通常,由于单独的编译模型,这意味着编译器在某种程度上限制了哪些库调用具有可观察的行为并且可以被消除.
至于它的深度,它取决于库的实现.在gcc中,C标准库使用编译器属性来通知编译器潜在的副作用(或者没有它们).例如,strlen标记有纯属性,允许编译器转换此代码:
char p[] = "Hi there\n";
for ( int i = 0; i < strlen(p); ++i ) std::cout << p[i];
Run Code Online (Sandbox Code Playgroud)
成
char * p = get_string();
int __length = strlen(p);
for ( int i = 0; i < __length; ++i ) std::cout << p[i];
Run Code Online (Sandbox Code Playgroud)
但是如果没有纯属性,编译器就无法知道函数是否有副作用(除非它是内联函数,并且可以看到函数内部),并且无法执行上述优化.
也就是说,一般情况下,编译器不会删除代码,除非它能证明它没有副作用,即不会影响程序的结果.请注意,这不仅与volatileio有关,因为任何变量都可能在以后具有可观察的行为.
对于问题3,编译器只会删除你的代码,如果程序的行为与代码存在完全一样(复制省略是一个例外),所以你甚至不应该关心编译器是否删除它.关于问题4,as-if规则代表:如果编译器产生的隐式重构的结果产生相同的结果,则可以自由地执行更改.考虑:
unsigned int fact = 1;
for ( unsigned int i = 1; i < 5; ++i ) fact *= i;
Run Code Online (Sandbox Code Playgroud)
编译器可以用以下代码自由替换该代码:
unsigned int fact = 120; // I think the math is correct... imagine it is
Run Code Online (Sandbox Code Playgroud)
环路走了,但行为是一样的:每次循环的互动不影响程序的结果,与变量在循环,即结束了正确的值,如果它在某些以后使用可观察到的操作中,结果将是- 如果循环已执行.
不要担心什么太多的可观察行为和作为,如果规则的意思是,他们基本上意味着,编译器必须产生你在你的代码编程的输出,即使它是免费的通过不同的路径到达这一结果.
编辑
@Konrad提出了关于最初的例子我有一个很好的点strlen:如何编译器知道该strlen呼叫可以被省略?答案是,在最初的例子中它不能,因此它不能忽略这些调用.没有什么可以告诉编译器从get_string()函数返回的指针不会引用正在其他地方修改的内存.我已经更正了示例以使用本地数组.
在修改示例中,数组是本地的,并且编译器可以验证没有其他指针引用相同的内存.strlen采用const指针,因此它承诺不修改包含的内存,并且该函数是纯的,所以它承诺不修改任何其他状态.数组不会在循环结构中修改,并且收集所有信息,编译器可以确定单个调用strlen就足够了.如果没有纯说明符,编译器就无法知道结果strlen在不同的调用中是否会有所不同,并且必须调用它.
标准定义的抽象机器将在给定特定输入的情况下产生一组特定输出中的一个。通常,所有保证的是对于该特定输入,编译后的代码将产生可能的特定输出之一。然而,魔鬼在细节中,有许多要点需要牢记。
其中最重要的可能是如果程序有未定义的行为,编译器绝对可以做任何事情。所有赌注都已关闭。编译器可以并且确实使用潜在的未定义行为进行优化:例如,如果代码包含类似的内容*p = (*q) ++,编译器可以得出结论,p并且q不是同一变量的别名。
未指定的行为可能会产生类似的影响:实际行为可能取决于优化级别。所需要的只是实际输出对应于抽象机器的可能输出之一。
关于volatile,标准确实说访问易失性对象是可观察的行为,但它将“访问”的含义留给了实现。在实践中,volatile这些日子你真的不能指望了。对 volatile 对象的实际访问在外部观察者看来可能与它们在程序中发生的顺序不同。(这可以说至少违反了标准的意图。然而,这是大多数现代编译器在现代架构上运行的实际情况。)
大多数实现将所有系统调用视为“IO”。当然,关于互斥体:就 C++03 而言,一旦启动第二个线程,就会出现未定义的行为(从 C++ 的角度来看——Posix 或 Windows 确实定义了它),并且在 C++11 中,同步原语是语言的一部分,并限制可能的输出集。(当然,如果编译器可以证明它们不是必需的,那么它可以消除同步。)
在new与delete运营商的特殊情况。它们可以由用户定义的版本代替,并且那些用户定义的版本可能明显具有可观察的行为。编译器只有在知道它们没有被替换或者替换没有可观察到的行为时才能删除它们。在大多数系统中,替换是在链接时定义的,在编译器完成其工作后,因此不允许更改。
关于你的第三个问题:我认为你从错误的角度看它。编译器不会“消除”代码,程序中的任何特定语句都不会绑定到特定的代码块。你的程序(完整的程序)定义了一个特定的语义,编译器必须做一些事情来产生一个具有这些语义的可执行程序。对于编译器编写者来说,最明显的解决方案是分别获取每个语句,并为其生成代码,但这是编译器编写者的观点,而不是您的观点。你把源代码放进去,然后得到一个可执行文件;但是很多语句不会产生任何代码,即使对于那些产生代码的语句,也不一定存在一对一的关系。从这个意义上说,“防止一些代码消除”的想法是没有意义的:您的程序具有由标准指定的语义,您所能要求的(以及您应该感兴趣的)就是最终的可执行文件具有这些语义。(您的第四点类似:编译器不会“删除”任何代码。)