C ++优化器重新排序对clock()的调用是否合法?

时间:2014-10-04 06:37:29

标签: c++ optimization clock

The C++ Programming Language 第4版,第225页读取:只要结果与简单执行顺序的结果相同,编译器就可以重新排序代码以提高性能。一些编译器,例如发布模式下的Visual C ++将重新排序此代码:

#include <time.h>
...
auto t0 = clock();
auto r  = veryLongComputation();
auto t1 = clock();

std::cout << r << "  time: " << t1-t0 << endl;

进入这种形式:

auto t0 = clock();
auto t1 = clock();
auto r  = veryLongComputation();

std::cout << r << "  time: " << t1-t0 << endl;

保证与原始代码不同的结果(零与大于零时间报告)。有关详细示例,请参阅my other question。这种行为是否符合C ++标准?

7 个答案:

答案 0 :(得分:18)

嗯,有一种名为Subclause 5.1.2.3 of the C Standard [ISO/IEC 9899:2011]的东西表明:

  

在抽象机器中,所有表达式都按照指定的方式进行评估   语义学。实际的实现不需要评估部分内容   表达式,如果它可以推断出它的值没有被使用而且没有   产生所需的副作用(包括通过调用a引起的任何副作用)   功能或访问易失性对象。)

因此,我真的怀疑此行为 - 您描述的那个 - 符合标准

此外 - 重组确实会对计算结果产生影响,但是如果从编译器的角度来看它 - 它存在于int main()世界中并且在进行时间测量时 - 它会窥视,请求内核给它当前时间,然后回到主要世界,外面世界的实际时间并不重要。 clock()本身不会影响程序,变量和程序行为也不会影响clock()函数。

时钟值用于计算它们之间的差异 - 这就是您要求的。如果在两次测量之间发生了某些事情,那么从编译器的角度来看是不相关的,因为你要求的是时钟差异,测量之间的代码不会影响测量过程。

然而,这并没有改变所述行为非常不愉快的事实。

尽管不准确的测量结果令人不愉快,但它可能会变得更糟,甚至更危险。

考虑以下代码来自this site

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()世界中思考并认为memset是一个死存储,因为之后不使用变量pwd并且不会影响程序本身。

答案 1 :(得分:10)

编译器无法交换两个clock调用。必须在t1之后设置t0。两种调用都是可观察到的副作用。只要观察结果与抽象机器的可能观察结果一致,编译器就可以对这些可观察效果之间的任何内容进行重新排序,甚至可以对可观察到的副作用进行重新排序。

由于C ++抽象机器并未正式限制为有限速度,因此它可以在零时间内执行veryLongComputation()。执行时间本身未定义为可观察的效果。真正的实现可能与之匹配。

请注意,很多答案取决于C ++标准而不是对编译器施加限制。

答案 2 :(得分:7)

是的,这是合法的 - 如果,编译器可以看到clock()来电之间发生的全部代码。

答案 3 :(得分:4)

如果veryLongComputation()在内部执行任何opaque函数调用,则为no,因为编译器无法保证其副作用可与clock()的副作用互换。

否则,是的,它是可以互换的 这是您使用时间不是一流实体的语言所支付的价格。

请注意,内存分配(例如new)可以属于此类别,因为分配函数可以在不同的转换单元中定义,并且在编译当前转换单元之前不会进行编译。所以,如果你只是分配内存,编译器就会被迫将分配和释放视为最坏情况下的一切障碍 - clock(),内存障碍和其他一切 - 除非它已经拥有内存代码分配器并且可以证明这不是必需的。在实践中,我认为任何编译器都不会真正查看分配器代码以试图证明这一点,因此这些类型的函数调用在实践中起到了障碍。

答案 4 :(得分:2)

至少在我的阅读中,不,这是不允许的。标准的要求是(§1.9/ 14):

  

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

编译器可以自由重新排序的程度由“as-if”规则(§1.9/ 1)定义:

  

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

这就留下了问题:有问题的行为(cout写的输出)是否是官方可观察到的行为。简短的回答是,是(§1.9/ 8):

  

符合实施的最低要求是:
  [...]
   - 在程序终止时,写入文件的所有数据应与根据抽象语义产生的程序执行的可能结果之一相同。

至少当我读到它时,这意味着与执行长计算相比,clock的调用可以重新排列,当且仅当它仍然产生相同的输出以按顺序执行调用时。

但是,如果您想采取额外措施来确保正确的行为,您可以利用另一项规定(也是§1.9/ 8):

  

- 严格按照抽象机的规则评估对volatile对象的访问。

要利用这一点,您需要稍微修改一下代码,以便:

auto volatile t0 = clock();
auto volatile r  = veryLongComputation();
auto volatile t1 = clock();

现在,我们不必将结论基于标准的三个单独部分,而且仍然只有一个相当某个答案,我们可以只看一个句子,然后有一个绝对某些答案 - 使用此代码,重新排序使用clock vs.,长时间计算明确禁止。

答案 5 :(得分:1)

假设序列处于循环中,并且veryLongComputation()随机抛出异常。那么将计算多少个t0和t1?它是否预先计算随机变量并根据预先计算重新排序 - 有时重新排序,有时不重新排序?

编译器是否足够聪明,只知道内存读取是从共享内存中读取的。读数是衡量控制棒在核反应堆中移动了多远的量度。时钟调用用于控制它们的移动速度。

也许时间正在控制哈勃望远镜镜子的磨削。 LOL

移动时钟调用似乎太危险了,无法留给编译器编写者的决定。因此,如果它是合法的,也许标准是有缺陷的。

IMO。

答案 6 :(得分:-1)

肯定允许,因为正如您所指出的那样,它会改变程序的可观察行为(不同的输出)(我不会进入{{1}的假设情况可能不会消耗任何可测量的时间 - 考虑到函数的名称,可能不是这样。但即使是这样,也没关系)。您不希望重新排序veryLongComputation()fopen,是否允许。

fwritet0都用于输出t1。因此,必须执行t1-t0t0的初始化表达式,并且必须遵循所有标准规则。使用了函数的结果,因此不可能优化函数调用,虽然它不直接依赖于t1,反之亦然,所以人们可能天真地倾向于认为移动它是合法的,为什么不呢。也许在t1的初始化之后,这不依赖于计算?
然而,间接地,t1 的结果当然取决于t1的副作用(特别是计算需要时间,如果没有别的话),这正是其中之一存在“序列点”之类的原因。

有三个“表达结束”序列点(加上三个“函数结束”和“初始化结束”SP),并且在每个序列点保证先前评估的所有副作用都已执行,并且尚未进行后续评估的副作用 如果你绕过这三个陈述,你就无法遵守这个承诺,因为所谓的函数的可能副作用都是未知的。如果编译器可以保证它将保持承诺,则只允许编译器进行优化。它不能,因为库函数是不透明的,它们的代码不可用(veryLongComputation()中的代码,必然在该翻译单元中已知)。

编译器有时会对库函数有“特殊知识”,例如某些函数不会返回或可能返回两次(想想veryLongComputationexit)。 但是,由于每个非空的,非平凡的函数(和setjmp在其名称上都非常重要)消耗时间,编译器具有关于否则不透明的“特殊知识”实际上,veryLongComputation库函数必须明确禁止重新排序此函数,因为知道不仅可以这样做,而且影响结果。

现在有趣的问题是为什么编译器会这样做呢?我可以想到两种可能性。也许你的代码触发了“看起来像基准”的启发式,编译器试图欺骗,谁知道。这不是第一次(想想SPEC2000 / 179.art,或SunSpider两个历史性的例子)。另一种可能性是clock内的某个地方,你无意中调用了未定义的行为。在这种情况下,编译器的行为甚至是合法的。