43

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

另请参阅此相关问题,该问题很接近但并不能完全回答我自己的问题。

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

4

4 回答 4

33

GCC 有两个属性,pureconst, 可用于标记此类功能。如果函数没有副作用并且其结果仅取决于其参数,则应声明该函数const,如果结果还可能依赖于某个全局变量,则应声明该函数pure。最近的版本还有一个-Wsuggest-attribute 警告选项,可以指出应该声明的函数constpure.

于 2013-04-26T09:57:49.690 回答
13

事实上,当今常见的编译器将执行您所询问的那种循环不变代码运动优化。有关这一点的演示,请参阅本文中题为“它会优化吗?”的第二个练习。,或使用gcc -S -O3和/或clang -S -O3组装下面的示例并检查组装中的main入口点,就像我出于好奇所做的那样。如果你的 VS2010 编译器没有执行这个优化,没关系;llvm/clang“与 MSVC 2010、2012、2013 和 14 CTP 集成”

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

§5.1.2.3p4:

在抽象机中,所有表达式都按照语义的规定进行评估。如果一个实际的实现可以推断出它的值没有被使用并且没有产生所需的副作用(包括调用函数或访问易失性对象引起的任何副作用),则它不需要评估表达式的一部分。

§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"); }

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

为了演示在实践中应用的理论知识,我通过在我的 Windows 10 系统上组装(使用/ )上面的代码测试了llvm/clang(3.8.0 from clang --version)和 gcc(6.4.0 from ),这两个编译器都有效地应用了优化如上所述; 在实践中,您可以期望从上面的示例中转换为等效于.gcc --versiongcc -S -O3clang -S -O3mainint main(void) { printf("%f", 0.0); }

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

于 2013-04-26T11:35:18.627 回答
7

允许在循环外提升这个子表达式所需的不是纯度,而是幂等性。

幂等性意味着一个函数如果被调用一次就会有相同的副作用和结果,就像用相同的参数多次调用它一样。因此,编译器可以将函数调用放在循环之外,仅受条件保护(循环是否会至少迭代一次?)。提升优化后的实际代码将是:

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

不过要小心,因为幂等性只适用于没有干预指令的重复调用。编译器必须执行数据流分析以证明函数和中间代码不交互(例如,中间代码仅使用从未占用地址的局部变量),然后才能利用幂等性。当已知函数是纯函数时,这不是必需的。但是纯度是一个更强大的条件,不适用于很多功能。

于 2013-04-26T18:40:36.433 回答
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

我希望我是对的。

于 2013-04-26T10:07:02.227 回答