【问题标题】:Do C and C++ optimizers typically know which functions have no side effects?C 和 C++ 优化器通常知道哪些函数没有副作用吗?
【发布时间】:2013-04-20 10:54:03
【问题描述】:

对于非常常见的数学函数,例如 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,它很接近但并不能完全回答我自己的问题。

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

【问题讨论】:

  • 它是特定于编译器的,但我在 gcc 文档的某处看到了 pure 属性。
  • @RogerRowland:它没有。考虑修改单例状态或可变变量
  • 这是我平时做的一种优化依赖编译器来做。手动完成通常很容易,因此几乎不需要让自己受制于编译器——它会受到许多其他因素的影响。
  • @Mysticial 我同意。您可以安全地做到这一点,而不会污染封闭范围for(size_t i = 0, end = foo.size(); i &lt; end; ++i).
  • @Koushik:我们这里说的优化其实是提升;但是,正如所指出的,您只能在执行一次函数并缓存其结果时才能提升函数,并产生与每次迭代执行时相同的输出。

标签: c++ c optimization


【解决方案1】:

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

【讨论】:

  • 请参阅lwn.net/Articles/285332,了解有关隐含优化的一些讨论。
  • 我很确定这个答案得到了 pureconst 的倒退。
  • 哦,没关系,关于 GCC 属性的答案是正确的,它不遵循 CS 术语。 pure 属性用于标记同样无副作用的幂等函数,const 属性用于标记纯函数。不知道谁认为这是个好主意。
  • @adl erm,我的也是。无论如何,您已经对答案和您的 lwn 链接表示赞成。所以编译器不能像 OP 想要的那样积极地提升数学调用,因为这些函数确实有副作用。
【解决方案2】:

事实上,当今的常见编译器将执行您所询问的那种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:

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

§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都不会导致任何需要的副作用。请注意,它们可能(并且可能确实)仍然会导致副作用,但它们不是需要来生成此程序的输出。

为了展示在实践中应用的理论知识,我通过组装(使用gcc -S -O3/clang -S -O3)测试了llvm/clang(来自clang --version的3.8.0)和gcc(来自gcc --version的6.4.0)上面的代码在我的 Windows 10 系统上,两个这两个编译器都有效地应用了上述优化;在实践中,您可以期望上面示例中的 main 被转换为与 int main(void) { printf("%f", 0.0); } 等效的机器代码。

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

【讨论】:

  • +1,我提到了通常执行的优化,而我可能应该将范围缩小到我正在使用的特定编译器。我将问题的范围稍微扩大了一些,因为我对其他常用编译器的作用也很感兴趣。
  • 那个程序最好不要打印"0.0\n"。也许您打算使用PI 而不是180.0
  • @BenVoigt 我的错。已更正。谢谢:)
【解决方案3】:

允许在循环外提升此子表达式所需的不是纯度,而是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 的副作用。

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

【讨论】:

    【解决方案4】:

    我想,是的。 如果你得到编译器反汇编输出,你可以看到,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
    

    我希望我是对的。

    【讨论】:

    • sin() 和大多数内置函数不同,因为编译器已经知道它们是纯函数。但是,当前的编译器通常会阻塞较大的程序员制作的函数。
    猜你喜欢
    • 2018-11-02
    • 1970-01-01
    • 2011-07-08
    • 1970-01-01
    • 2021-11-22
    • 1970-01-01
    • 2015-08-12
    • 2010-10-30
    相关资源
    最近更新 更多