我怎样才能省去对幂等函数的额外调用?

时间:2014-08-22 21:17:23

标签: c++ c gcc

有没有办法告诉gcc如果两个后续调用具有相同的参数,则只应调用一次具有副作用的函数。我想要以下行为:

foo(6);//run this function
foo(6);//optimize this away
foo(6);//optimize this away
foo(5);//run this function
foo(6);//run this function again

我可以让foo在做任何工作之前检查一个全局变量,但这不是最佳的。

void inline foo(int i){
   static int last_i=i+1;
   if(last_i != i){
        last_i==i;
        //do_work...
    }
}

由于foo是一个内联函数,编译器应该能够查看foo()的调用,并发现它不必执行它。问题是编译器不能像全局变量一样进行优化,有没有办法让编译器知道这样做是否安全?

6 个答案:

答案 0 :(得分:7)

  

...如果两个后续调用具有相同的参数,则只应调用一次具有副作用的函数...

虽然它具有副作用,但该功能必须是幂等的。

C ++标准仅区分具有副作用(I / O功能)和没有副作用的功能。从编译器的角度来看,如果函数是不透明的(在同一个翻译单元中没有定义)那么它必须有副作用,因此它是编译器内存屏障,编译器无法优化调用或推断返回值(除非它是编译器内部函数,如memcpy)。

Idempotence, computer science meaning

  

在计算机科学中,术语“幂等”被更全面地用于描述如果执行一次或多次将产生相同结果的操作。[4]这可能具有不同的含义,具体取决于应用它的上下文。例如,在具有副作用的方法或子程序调用的情况下,这意味着在第一次调用之后修改的状态保持不变。但是在函数式编程中,幂等函数是对任何值x具有属性f(f(x))= f(x)的函数。[5]

而C ++没有这个概念。

答案 1 :(得分:5)

您可以使用static变量:

int foo(int param){
   static int last=0;
   static int result=1;
   if(last==param) return result;
   else{
      last=param;
      result=param/2+1;
      return result;
   }
}

答案 2 :(得分:3)

为了让编译器优化掉具有副作用的函数,它必须理解它产生的副作用。 GCC没有注释来描述副作用的类型,因此不可能。

如果函数在同一编译单元中,编译器可能能够确定调用是多余的,但是只有在函数足够简单以便编译器完全理解哪种情况很少的情况下才能工作。您最好将该逻辑放入调用者或被调用者。

为了完整性,如果函数有副作用,您可以使用__attribute__((pure))__attribute__((const))告诉编译器。

答案 3 :(得分:2)

您可以使用仿函数:

class{
        auto foo(int a);
        int last_arg;
        bool first_invoke=true;
    public:
        auto operator(int a){
            if(first_invoke){
                first_invoke=false;
                last_arg=a;
                return foo(a);
            }
            if(a==last_arg)
                //do_something_special;
            else{
                last_arg=a;
                return foo(a);
            }
        }
}foo;

此解决方案依赖于标准,而不依赖于编译器。

或者您可以使用foo static个可选项进行游戏。

答案 4 :(得分:2)

没有。这种行为不是我见过的许多语言的语义的一部分,当然也不是C / C ++。 Gcc无法提供用错误的语义行为编译代码的选项!

然而,反过来是可能的。如果函数foo()是"纯粹的"即没有副作用,那么一个好的编译器将处理例如y = foo(x)+ foo(x);仅使用一次调用foo()。 Ada语言为此目的提供了一种主张纯洁的语用。

答案 5 :(得分:0)

所以我犹豫不决只是对最后使用的参数进行检查是因为函数调用是在一些非常紧密的内部循环中,所以额外的比较和分支指令会很烦人,特别是在平台上(或者非常差) )分支预测。

我决定看看gcc在尝试时做了什么。我使用了以下代码:

#include <stdio.h>
int check;

void myfun(int num){
        printf("changing to %d\n",num);
}
static inline __attribute__((always_inline)) void idem(int num){
    if(num!=check){
        myfun(num);
        check=num;
    }
}

int main(){
    idem(5);
    idem(5);
    idem(4);
    idem(4);
    return 0;
}

这会将x86(不是我的最终目标)上的(gcc -O2 main.c)编译为:

0000000000400440 <main>:
  400440:       48 83 ec 08             sub    $0x8,%rsp
  400444:       83 3d e5 0b 20 00 05    cmpl   $0x5,0x200be5(%rip)        # 601030 <check>
  40044b:       74 14                   je     400461 <main+0x21>
  40044d:       bf 05 00 00 00          mov    $0x5,%edi
  400452:       e8 09 01 00 00          callq  400560 <myfun>
  400457:       c7 05 cf 0b 20 00 05    movl   $0x5,0x200bcf(%rip)        # 601030 <check>
  40045e:       00 00 00 
  400461:       bf 04 00 00 00          mov    $0x4,%edi
  400466:       e8 f5 00 00 00          callq  400560 <myfun>
  40046b:       c7 05 bb 0b 20 00 04    movl   $0x4,0x200bbb(%rip)        # 601030 <check>
  400472:       00 00 00 
  400475:       31 c0                   xor    %eax,%eax
  400477:       5a                      pop    %rdx
  400478:       c3                      retq   
  400479:       90                      nop
  40047a:       90                      nop
  40047b:       90                      nop

所以你可以看到myfun只被调用了两次,就像我想要的那样。因此,看起来gcc可以正确地执行此操作。如果有人想对这里的优化有任何限制,我会非常感兴趣