C和C ++优化器通常知道哪些函数没有副作用吗?

对于非常常见的math函数,比如sin,cos等等,编译器是否意识到它们没有副作用,并且能够将它们移动到外部循环? 例如

// Unoptimized double YSinX(double x,int y) { double total = 0.0; for (int i = 0; i < y; i++) total += sin(x); return total; } // Manually optimized double YSinX(double x,int y) { double total = 0.0, sinx = sin(x); for (int i = 0; i < y; i++) total += sinx; return total; } 

如果可以的话,有没有一种方法可以声明一个没有副作用的函数,因此可以通过这种方式进行优化? 对VS2010应用程序的初步分析表明,优化是有益的。

另请参阅这个相关的问题 ,这是接近,但不完全回答我自己的问题。

编辑:一些伟大的答案。 我接受的一个是基于它所引发的评论本身,尤其是链接的文章,以及在确定errnoerrno )的情况下(即副作用)可能不会出现提升的事实。 因此,在我正在做的事情的背景下,这种types的手动优化似乎仍然有意义。

GCC有两个属性 , pureconst ,可以用来标记这样的function。 如果该函数没有副作用,而且其结果仅取决于其参数,则该函数应声明为const ,如果结果也可能取决于某个全局variables,则该函数应声明为pure 。 最近的版本也有一个-Wsuggest-attribute 警告选项 ,可以指出应该被声明为constpure const

事实上,今天的通用编译器会执行你所问的那种循环不变的代码运动优化。 为了演示这一点,请参阅本文中的第二个练习, 题目是“它会优化吗? 。 如果你的VS2010编译器不执行这个优化,不要紧; LLVM / Clang“与MSVC 2010,2012,2013和14 CTP集成” 。

从理论的angular度来看,这两个引用解释了编译器执行优化时的范围或空间。 他们来自C11标准。 IIRC C ++ 11有类似的东西。

§5.1.2.3p4:

在抽象机器中,所有expression式都按照语义指定的方式进行评估。 如果一个实际的实现可以推断出它的值没有被使用,并且没有产生所需的副作用(包括由调用一个函数或者访问一个volatile对象引起的任何副作用),那么它就不需要评估一个expression式的一部分。

§5.1.2.3p6:

符合实现的最低要求是:

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

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

– 交互设备的input和输出dynamic应按照7.21.3的规定进行。 这些要求的意图是尽可能快地出现无缓冲或线路缓冲输出,以确保提示消息实际上出现在等待input的程序之前。

这是该程序的可观察行为。

因此,如果可以的话,编译器可能会将整个程序提升为编译时评估。 考虑下面的程序,例如:

 double YSinX(double x,int y) { double total = 0.0; for (int i = 0; i < y; i++) total += sin(x); return total; } int main(void) { printf("%lf\n", YSinX(PI, 4)); } 

您的编译器可能会意识到这个程序每次打印0.0\n ,并优化您的程序到:

 int main(void) { puts("0.0"); } 

也就是说, 提供你的编译器可以certificatesinYsinX都不会导致任何需要的副作用。 请注意,他们可能(也可能是)仍然会导致副作用,但不需要产生此程序的输出。

你问了一个关于“编译器”的问题。 如果您指的是所有的C或C ++实现 ,就没有保证的优化,并且C实现甚至不需要编译器。 您需要告诉我们哪些特定的C或C ++实现 ; 正如我之前提到的,LLVM / Clang“与MSVC 2010,2012,2013和14 CTP集成在一起”,因此您可能会使用它。 如果您的C或C ++编译器不能生成最佳代码,请获得一个新的编译器(例如LLVM / Clang)或自行生成优化,最好通过修改您的编译器,以便您可以向开发人员发送补丁程序,并将优化自动传播到其他的项目。

允许将这个子expression式提升到循环之外需要的不是纯粹的,而是相等的 。

如果一个函数被调用一次,那么这个函数会有相同的副作用和结果,就好像它被多次调用相同的参数一样。 因此,编译器可以将函数调用放在循环之外,只受条件保护(循环会至less迭代一次?)。 然后提升优化后的实际代码将是:

 double YSinX(double x,int y) { double total = 0.0; int i = 0; if (i < y) { double sinx = sin(x); // <- this goes between the loop-initialization // first test of the condition expression // and the loop body do { total += sinx; i++; } while (i < y); } return total; } 

__attribute__(pure)idempotent之间的区别是很重要的,因为正如adl在他的评论中指出的那样,这些函数确实具有设置errno的副作用。

但是,要小心,因为幂等只适用于重复调用,没有中介指令。 编译器将不得不执行数据stream分析来certificate函数和中介代码之间没有交互(例如,中间代码只使用其地址从未被占用的本地代码),然后才能利用幂等性。 当函数已知是纯粹的时候,这是不必要的。 但纯度是一个更强大的条件,不适用于很多function。

我想是的。 如果你得到编译器的反汇编输出,你可以看到sin在另一个标签中被调用,而不是在for的循环标签中:(用g ++ -O1 -O2 -O3编译)

 Leh_func_begin1: pushq %rbp Ltmp0: movq %rsp, %rbp Ltmp1: pushq %rbx subq $8, %rsp Ltmp2: testl %edi, %edi jg LBB1_2 pxor %xmm1, %xmm1 jmp LBB1_4 LBB1_2: movl %edi, %ebx callq _sin ;sin calculated pxor %xmm1, %xmm1 .align 4, 0x90 LBB1_3: addsd %xmm0, %xmm1 decl %ebx jne LBB1_3 ;loops here till i reaches y LBB1_4: movapd %xmm1, %xmm0 

我希望我是正确的。