一个C ++优化器重新sorting调用clock()是否合法?
C ++编程语言第4版(第225页)表示: 只要结果与简单的执行顺序相同,编译器就可以对代码进行重新sorting以提高性能 。 一些编译器,例如发布模式下的Visual C ++,将重新排列这个代码:
#include <time.h> ... auto t0 = clock(); auto r = veryLongComputation(); auto t1 = clock(); std::cout << r << " time: " << t1-t0 << endl;
进入这种forms:
auto t0 = clock(); auto t1 = clock(); auto r = veryLongComputation(); std::cout << r << " time: " << t1-t0 << endl;
这保证了与原始代码不同的结果(零报告与大于零报时)。 看到我的其他问题的详细的例子。 这种行为是否符合C ++标准?
编译器不能交换两个clock
调用。 t1
必须在t0
之后设置。 这两个调用都是可观察的副作用。 只要观察结果与抽象机器可能的观察结果一致,编译器就可以对这些可观察效果之间的任何事物重新sorting,甚至可以对可观察的副作用进行重新sorting。
由于C ++抽象机器没有被正式限制在有限的速度上,它可以在零时间内执行veryLongComputation()
。 执行时间本身并没有被定义为可观察到的效果。 真正的实现可能会匹配。
请注意,许多这样的答案取决于C ++标准, 而不是对编译器施加限制。
那么Subclause 5.1.2.3 of the C Standard [ISO/IEC 9899:2011]
中有一个叫Subclause 5.1.2.3 of the C Standard [ISO/IEC 9899:2011]
说明:
在抽象机器中,所有expression式都按照语义指定的方式进行评估。 如果一个实际的实现可以推断出它的值没有被使用,并且没有产生所需的副作用(包括由调用一个函数或者访问一个volatile对象引起的任何副作用),那么它就不需要评估一个expression式的一部分。
所以我真的怀疑这个行为 – 你所描述的 – 是符合标准的 。
此外,重组确实会对计算结果产生影响,但是如果从编译器的angular度来看它的话,它就存在于int main()
世界中,当进行时间测量的时候,它会窥视,要求内核给它当前的时间,回到主要的世界,外面的世界实际上并不重要。 clock()本身不会影响程序和variables,程序行为不会影响clock()函数。
时钟值用来计算它们之间的差异 – 这就是你所要求的。 如果发生了什么事情,在两次测量之间,从编译器的angular度来看是不相关的,因为你要求的是时钟差异,测量之间的代码不会影响测量过程。
然而这并不改变所描述的行为是非常不愉快的事实。
即使不准确的测量不愉快,它可能变得更加糟糕甚至是危险的。
考虑下面的代码从这个网站 :
void GetData(char *MFAddr) { char pwd[64]; if (GetPasswordFromUser(pwd, sizeof(pwd))) { if (ConnectToMainframe(MFAddr, pwd)) { // Interaction with mainframe } } memset(pwd, 0, sizeof(pwd)); }
正常编译时,一切正常,但如果应用优化,memset调用将被优化,这可能会导致严重的安全漏洞。 为什么它被优化了? 这很简单, 编译器再次在其main()
世界中认为,由于variablespwd
之后不被使用,并且不会影响程序本身,所以认为memset是一个死存储区。
是的,这是合法的 – 如果编译器可以看到clock()
调用之间发生的所有代码。
如果veryLongComputation()
内部执行任何不透明的函数调用,那么不会,因为编译器不能保证其副作用可以与clock()
副作用互换。
否则,是的,它是可以互换的。
这是使用时间不是一stream实体的语言所付出的代价。
请注意,内存分配(如new
)可能属于此类别,因为可以在不同的翻译单元中定义分配函数,并且在编译当前翻译单元之前不会编译分配函数。 所以,如果你只是分配内存,编译器就不得不把分配和释放视为最坏的障碍 – clock()
,内存障碍和其他一切 – 除非它已经有了内存分配器的代码,可以certificate这是没有必要的。 在实践中,我不认为任何编译器实际上都会查看分配器代码来试图certificate这一点,所以这些types的函数调用在实践中成为障碍。
至less通过我的阅读,不,这是不允许的。 标准的要求是(§1.9/ 14):
每一个与全expression式相关的数值计算和副作用在每一个与下一个要被评估的全expression式相关的数值计算和副作用之前被sorting。
编译器自由重新sorting的程度由“as-if”规则(§1.9/ 1)定义:
本国际标准对合规实施的结构没有要求。 特别是,他们不需要复制或模拟抽象机器的结构。 相反,需要符合的实现来模拟(仅)抽象机器的可观察行为,如下所述。
这就留下了问题行为( cout
所写的输出)是否是官方可观察到的行为。 简单的回答是,是(§1.9/ 8):
符合实现的最低要求是:
[…]
– 在程序终止时,写入文件的所有数据应与根据抽象语义执行程序所产生的结果相同。
至less在我读到的时候,这意味着对于clock
的调用可以重新排列,与执行长计算相比,当且仅当它仍然产生相同的输出来执行调用。
但是,如果您想采取额外步骤来确保正确行为,则可以利用另一项规定(也是§1.9/ 8):
– 访问易失性对象严格按照抽象机器的规则进行评估。
要利用这一点,你可以稍微修改你的代码,使其成为如下所示:
auto volatile t0 = clock(); auto volatile r = veryLongComputation(); auto volatile t1 = clock();
现在,我们不必把结论基于标准的三个单独的部分,而只有一个相当确定的答案,我们可以看到一个句子,并且有一个绝对肯定的答案 – 用这个代码重新sortingclock
与clock
对比, 明显禁止长时间计算。
假设序列在一个循环中,并且veryLongComputation()随机抛出一个exception。 那么将会计算多less个t0和t1? 是否预先计算随机variables,并根据预先计算重新sorting – 有时会重新sorting,有时候不会。
编译器是否足够聪明,知道只读内存是从共享内存读取的。 读数是衡量控制棒在核反应堆中移动的程度。 时钟调用用于控制移动的速度。
或者,也许时机正在控制哈勃望远镜的研磨。 大声笑
移动时钟调用似乎太危险了,不会留给编译器编写者的决定。 所以如果是合法的,也许这个标准是有缺陷的。
IMO。
这肯定是不允许的,因为它会改变,正如你所指出的,程序的可观察行为(不同的输出)(我不会进入假设的情况, veryLongComputation()
可能不会消耗任何可测量的时间 -名字,大概不是这样,但即使是这样的话,也不是真的很重要)。 你不会指望它是可以重新排列fopen
和fwrite
,你会。
t0
和t1
都用于输出t1-t0
。 因此,必须执行t0
和t1
的初始化expression式,并且这样做必须遵循所有标准规则。 函数的结果被使用,所以不可能优化出函数调用,虽然它不直接依赖t1
,反之亦然,所以人们可能天真地认为合法的移动它,为什么不。 也许在t1
的初始化之后,这不依赖于计算?
然而,间接地, t1
的结果当然依赖于veryLongComputation()
副作用(特别是计算时间,如果没有其他的话),这正是存在“序列点”这样的事情的原因之一。
有三个“expression式结束”序列点(加上三个“function结束”和“初始化结束”SP),并且在每个序列点保证以前评估的所有副作用将被执行,并且没有侧面来自后续评估的效果尚未完成。
如果围绕这三个陈述进行移动,则没有办法遵守这个承诺,因为所有被调用函数的可能的副作用都是未知的 。 编译器只有在能够保证它能保持承诺的情况下才被允许优化。 它不能,因为库函数是不透明的,它们的代码是不可用的(非常长计算中的代码也不一定在那个翻译单元中是已知的)。
然而,编译器有时对库函数有“特殊的知识”,比如某些函数不会返回或者可能返回两次(想想exit
或setjmp
)。
然而,由于每一个非空的非平凡函数(以及很长的veryLongComputation
是非常不平凡的) 都会消耗时间,所以具有关于不透明clock
库函数的“特殊知识”的编译器事实上将不得不被明确地禁止从围绕这一个重新sorting电话,知道这样做不仅可能,但会影响结果。
现在有趣的问题是编译器为什么要这样做呢? 我可以想到两种可能性。 也许你的代码触发一个“看起来像基准”的启发式和编译器试图欺骗,谁知道。 这不会是第一次(想想SPEC2000 / 179.art,或SunSpider的两个历史例子)。 另一种可能性是在veryLongComputation()
内的某个地方,你无意中调用未定义的行为。 在这种情况下,编译器的行为甚至是合法的。