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


71

C ++编程语言第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;

这样可以保证结果与原始代码不同(报告的时间为零与大于零的时间)。请参阅我的其他问题以获取详细示例。这种行为符合C ++标准吗?


2
不它不是。编译器应期望函数具有一些副作用。一些编译器具有“纯常数”功能的语言扩展。您可以要求编译器显示汇编代码(例如,g++ -O2 -S -fverbose-asm your-code.cc使用GCC ...)
Basile Starynkevitch 2014年

7
“这保证了与原始代码不同的结果(报告了零与大于零的时间)” -就标准而言,这并不保证,因为该标准未提及任何特定操作应花费多长时间(除了调用睡眠功能)。呼叫veryLongComputation()很可能是即时的。
本杰明·林德利

1
@BenjaminLindley-The call to veryLongComputation() could very well be instantaneous我不同意。有大量的算法,如果使用编写C ++标准时已知的任何计算硬件,在给定足够大的数据的情况下,保证不会在太阳系结束之前完成所有算法。
Paul Jurczak 2014年

3
@Paul符合I / O要求的是发送到发送部分,而不是计算r部分。您所看到的完全符合要求。无论您在时钟调用之间看到的任何非负差异,可能很小,都可以四舍五入为零,这很好。想象一下一个程序,其中两次调用Clock之差使得您以大约50%的概率获得0或1,这取决于启动的时间和运行的时间。优化是否应该保留这种确切的可能性?
Marc Glisse 2014年

2
从抽象机的角度来看,您如何分辨两个版本之间的区别?不能保证计算将花费多长时间。(实际示例:编译器可以完全评估您的计算。)。如果您的计算在保证的最小持续时间(例如睡眠)中执行了副作用,则此重新排序将无效。IO是不够的,因为同样,您无法分辨重新排序和真正快速的IO之间的区别。视情况规则适用。
usr 2014年

Answers:


14

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

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

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


非易失性变量的分配不是可观察到的副作用。
Pete Becker

2
应该更明确:两个调用都是可观察到的副作用。
MSalters 2014年


18

好吧,有个叫Subclause 5.1.2.3 of the C Standard [ISO/IEC 9899:2011]什么的状态:

在抽象机中,所有表达式均按语义指定的方式求值。如果实际实现可以推断出未使用表达式的值并且没有产生所需的副作用(包括由调用函数或访问易失性对象引起的副作用),则无需评估表达式的一部分。

因此,我真的怀疑这种行为-您所描述的行为-符合该标准

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

时钟值用于计算它们之间的差异-这就是您要的。如果正在发生某种情况,那么从编译器的角度来看,两次测量之间就没有关系,因为您要求的是时钟差,并且两次测量之间的代码不会影响到整个过程。

但是,这不会改变所描述的行为非常令人不愉快的事实。

即使不准确的测量令人不快,它也可能变得更加糟糕甚至危险。

考虑以下从该站点获取的代码:

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不使用该变量并且不会影响程序本身。


另一方面,它实际上确实会影响功能。如果在其他位置调用它,则返回不同的值。而且,如果这不是由长时间的计算引起的副作用,我也不知道这是什么。是的,“测量之间的代码不会影响测量作为一个过程”。但这在很大程度上影响了价值。这就像说“变量的值不会改变求和过程”。
sharpneli 2014年

我认为总和不一样。您求和的变量几乎是精确的和确定性的。另一方面,时钟时间不是。实际上,时差测量的结果在某种意义上是随机的……
Jendas 2014年

2
@sharpneli“除非编译器可以证明[...]”,但这正是编译器在这里所​​做的。如果您从其中隐藏veryLongComputation的主体,我强烈怀疑它会重新排序。但是这里详细介绍了veryLongComputation,证明其中没有副作用,然后重新排序。不,不同的运行时间(挥舞着双臂说这是非常不同的)不算作副作用。
Marc Glisse 2014年

1
@MSalters:如果pwd是易失性的,则将其传递给memset未定义行为。您不能通过非易失性限定的左值来访问易失性对象,这将是memset这样做的。
R .. GitHub停止帮助ICE

5
点。std::fill会工作。易失性指针仍然是迭代器。这么多的C ++比C.更难以预测
MSalters

7

是的,这是合法的-如果编译器可以看到在clock()调用。


1
您是否有标准的报价或参考证明了这一点?

2
@remyabel原始问题中的引号允许使用。关键是要认识到对的调用clock不是要重新排序的,而是要进行的计算。根据规则,可以随时计算任何透明计算。
o11c 2014年

1
更重要的是,如果编译器可以保证不会veryLongComputation()导致可观察到的行为。
2014年

@ o11c原始问题中的引号允许-但结果不同,这是与该引号相矛盾的常识。与时间相关的数量是否不受此规则约束?
Paul Jurczak 2014年

2
@PaulJurczak是正确的;可观察到的行为是写易失性变量,并调用库函数
MM

4

如果在veryLongComputation()内部执行任何不透明的函数调用,则否,因为编译器无法保证其副作用可以与以下情况的互换:clock()

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

请注意,内存分配(例如new)可以归入此类,因为可以在不同的转换单元中定义分配函数,并且只有在当前转换单元已被编译后才可以对其进行编译。因此,如果您仅分配内存,则编译器将被迫将分配和释放视为所有情况clock()(包括内存障碍以及所有其他情况)的最坏情况障碍,除非它已经具有内存分配器的代码并且可以证明这是没有必要的。实际上,我认为没有任何编译器会实际查看分配器代码来尝试证明这一点,因此这些类型的函数调用实际上会成为障碍。


为什么功能不是一流实体是问题?
哈罗德2014年

@harold:关于函数是一流实体,我什么也没说。
user541686 2014年

2

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

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

编译器可以自由重新排序的程度超出“按条件”规则(第1.9 / 1节)所定义的范围:

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

剩下的问题是有关行为(由编写的输出cout)是否是正式可观察到的行为。简短的答案是,是的(第1.9 / 8节):

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

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

但是,如果您想采取额外的步骤来确保正确的行为,则可以利用另一项规定(另请参见第1.9 / 8节):

—严格根据抽象机的规则评估对易失对象的访问。

要利用此优势,您可以对代码进行一些修改,使其类似于:

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

现在,而不是立足于标准的三个独立的部分,仍然只具有结论相当一定的答案,大家可以看一下只有一个句子,有一个绝对的某些答案-与此代码,重新排序用途的clock对比,长计算是明确禁止的。


5
通过这种逻辑,即使优化1 + 12也是无效的,因为代码可能正在计时计算。我认为逻辑不正确。

您似乎阅读不正确。“与完整表达式关联的每个值计算和副作用在与要评估的下一个完整表达式关联的每个值计算和副作用之前进行排序。” 这与持续折叠毫无关系。
杰里·科芬

您的答案声称,如果代码正在计时计算和打印时间,则由于优化,精确的打印时间一定不能更改,因为可以观察到精确的打印时间。

不是这样 我的回答声称,它可以进行某些更改,例如更改表达式中子表达式的顺序或求值,而不能进行其他更改,例如完整表达式的求值顺序。
杰里·科芬

2
@JerryCoffin无论如何,这不是典型的语言律师问题:这与措辞无关,因为某些假设的实现可能会滥用不正确的措辞,这是真实世界中的编译器根据标准的确切措辞进行优化的地方,而且几乎每个优化编译器都具有类似的优化,即使它们未针对该特定测试程序显示。即使您得出结论(无论是对还是错)该标准都不打算允许这些优化,但希望可移植到当前实现中的代码也必须加以处理。

1

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

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

或者,时机正在控制哈勃望远镜镜的打磨。大声笑

移动时钟调用似乎太危险了,不能交给编译器编写者来决定。因此,如果合法,则该标准可能有缺陷。

海事组织。


如果计时很重要,则应该使用比不执行计时代码更值得信赖的机制。和/或禁用所有优化-优化器所做的大部分工作都涉及重新排序指令。
cHao 2014年

-1

这肯定是容许的,因为它的变化,因为你已经注意到,该方案的observeable行为(不同的输出)(我不会进入该假设情况veryLongComputation()可能不消耗任何可测量时间-定的函数的名称,是大概不是这样。但是即使是这样,也没关系。你不会想到,这是允许的要重新排序fopenfwrite,你会。

二者t0t1在输出中使用t1-t0。因此,对于初始化表达式t0t1必须执行,这样做必须符合所有标准的规则。使用了函数的结果,因此不可能优化函数调用,尽管它并不直接依赖于函数调用,t1反之亦然,因此人们可能会天真地认为将其移动是合法的,为什么不这样做呢? 。也许在初始化之后t1,不依赖于计算吗?
但是,间接的结果(特别是计算需要花费时间,如果没有其他情况),这恰好是存在诸如“序列点”之类的原因之一。t1 当然取决于副作用veryLongComputation()

有三个“表达式结尾”序列点(加上三个“函数结尾”和“初始化程序的结尾” SP),并且在每个序列点处都保证将执行先前评估的所有副作用,并且没有副作用后续评估的效果尚未执行。
如果在这三个语句中四处移动,您将无法兑现这一承诺,因为未知所有被调用函数的可能副作用。仅当编译器可以保证将遵守承诺时,才允许对其进行优化。它不能,因为库函数是不透明的,它们的代码是不可用(也就是内部的代码veryLongComputation必然在翻译单元已知的)。

但是,编译器有时确实具有关于库函数的“特殊知识”,例如某些函数将不会返回或可能会返回两次(认为exitsetjmp)。
但是,由于每个非空的,非平凡的函数(并且veryLongComputation从名称上来说都是非平凡的)都会消耗时间,clock因此实际上必须明确禁止具有其他方面不透明库函数的“特殊知识”的编译器知道这一点不仅可能而且影响结果。

现在有趣的问题是,为什么编译器仍然这样做?我可以想到两种可能性。也许您的代码触发了“看起来像基准”启发式方法,并且编译器试图作弊,谁知道。这将不是第一次(想想SPEC2000 / 179.art,或者两个历史示例都是SunSpider)。另一种可能性是veryLongComputation(),您无意间调用了内部未定义的行为。在这种情况下,编译器的行为甚至是合法的。


1
我认为您误以为MSVC没有看到的主体veryLongComputation。MSVC具有链接时间代码生成功能,从本质上讲,它可以看到该正文。
MSalters 2014年

@MSalters:可能是这样,但是在没有看到的代码的情况下这是无关紧要的clock。为了使优化合法,它必须能够证明任何功能都没有副作用,并且它不能这样做(因为它需要查看功能的每个功能)。此外,有必要使可观察的行为相同。事实也并非如此。
戴蒙2014年

对的调用clock 可观察到的(所有库调用都是)。无需进一步证明。不,行为不必与非优化版本相同。该标准仅要求结果与抽象机的可能结果之一相同,并且在一个单位内运行time_t绝对是可能的,因此是有效结果。
MSalters 2014年

Nitpick:库I / O调用是副作用。(第1.9 / 12节)不执行I / O的库调用不是。甚至I / O函数也不一定必须被调用,除非该调用影响了可观察的行为。
cHao 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.