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

时间:2013-04-26 09:48:23

标签: c++ c optimization

说非常常见的数学函数,例如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应用程序的初始分析表明优化是有益的。

另请参阅此related question,这篇文章很接近,但并不完全回答我自己的问题。

编辑:一些很棒的答案。我接受的那个基于它作为答案本身引起的评论,特别是链接的文章,以及在设置errno的情况下(即副作用)可能不会发生提升的事实。因此,在我正在做的事情中,这种类型的手动优化似乎仍然有意义。

4 个答案:

答案 0 :(得分:32)

GCC有两个attributespureconst,可用于标记此类功能。如果函数没有副作用且其结果仅取决于其参数,则该函数应声明为const,如果结果也可能依赖于某个全局变量,则应将函数声明为pure。最新版本还有一个-Wsuggest-attribute warning option,可以指出应该声明为constpure的函数。

答案 1 :(得分:13)

事实上,今天的常见编译器将执行您所询问的loop-invariant code motion优化。有关此示例,请参阅this article entitled "Will it Optimize?"中的第二个练习,或使用gcc -S -O3和/或clang -S -O3汇总下面的示例并检查程序集中的main入口点,因为我出于好奇而做了。如果您的VS2010编译器没有执行此优化,则无关紧要; llvm/clang "integrates with MSVC 2010, 2012, 2013 and 14 CTP"

从理论上来说,这两个引号解释了编译器在执行优化时所具有的范围或余量。它们来自C11标准。 IIRC C ++ 11有类似之处。

<强>§5.1.2.3p4:

  

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

<强>§5.1.2.3p6:

  

符合实施的最低要求是:

     

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

     

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

     

- 交互设备的输入和输出动态应采用   按照规定放置的地方   7.21.3。这些要求的目的是尽快出现无缓冲或行缓冲输出,以确保这一点   提示消息实际上出现在程序等待之前   输入

     

这是该计划的可观察行为。

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

#include <math.h>
#include <stdio.h>

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("%f\n", YSinX(M_PI, 4));
}

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

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

也就是说,提供您的编译器可以证明sinYsinX都不会导致任何所需的副作用。请注意,它们可能(并且可能确实)仍然会产生副作用,但它们不是需要来生成此程序的输出。

为了展示在实践中应用的理论知识,我通过汇编(使用{{}来测试llvm/clang(来自clang --version的3.8.0)和gcc(来自gcc --version的6.4.0)。 1}} / gcc -S -O3)我的Windows 10系统上面的代码,这些编译器的两个已经有效地应用了上述优化;实际上,您可以将上述示例中的clang -S -O3转换为相当于main的机器代码。

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

答案 2 :(得分:7)

允许在循环外部提升此子表达式所需的内容不是纯度,而是idempotence

Idempotence意味着一个函数将具有相同的副作用,并且如果它被调用一次,就好像它被多次使用相同的参数调用一样。因此,编译器可以将函数调用放在循环之外,仅受条件保护(循环至少迭代一次吗?)。然后,提升优化后的实际代码为:

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的副作用。

但要小心,因为幂等仅适用于没有干预指令的重复呼叫。编译器必须执行数据流分析以证明函数和插入代码不会相互作用(例如,介入代码仅使用其地址永远不会被占用的本地符号),然后才能利用幂等性。当已知该函数是纯的时,这不是必需的。但纯度是一个更强大的条件,不适用于很多功能。

答案 3 :(得分:6)

我想,是的。 如果你得到编译器反汇编输出,你可以看到,在另一个标签中调用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

我希望我是对的。