如何省略对幂等函数的附加调用?

How can I elide additional calls to an idempotent function?

本文关键字:调用 函数 何省略      更新时间:2023-10-16

是否有一种方法告诉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()的调用,并看到它不必执行它。问题是编译器不能优化全局变量,有没有办法让编译器知道它是安全的?

…有副作用的函数应该只在两个后续调用具有相同参数的情况下调用一次…

这个函数必须是幂等的尽管它有副作用。

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

幂等性,计算机科学意义:

在计算机科学中,幂等一词被更广泛地用于描述一种操作,该操作如果执行一次或多次将产生相同的结果。[4]根据应用上下文的不同,这可能有不同的含义。例如,在具有副作用的方法或子例程调用的情况下,这意味着在第一次调用后修改的状态保持不变。在函数式编程中,幂等函数是指对任意值x具有f(f(x)) = f(x)的函数。[5]

而c++没有这个概念

您可以使用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;
   }
}

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

如果函数在同一个编译单元中,编译器可能会发现调用是冗余的,但只有当函数足够简单,编译器才能完全理解时,这才有效,而这种情况很少发生。您最好将该逻辑放入调用方或被调用方。

为完整起见,如果函数没有有副作用,您可以使用__attribute__((pure))__attribute__((const))告诉编译器这一点。

No。这种行为不是我所见过的许多语言的语义的一部分,当然也不是C/c++。Gcc不能提供选项来编译具有错误语义行为的代码!

然而,反过来也是可能的。如果函数foo()是"纯"的,即没有副作用,那么一个好的编译器将处理eg y=foo(x)+foo(x);只使用一个对foo()的调用。Ada语言为此目的提供了一个断言纯粹性的pragma。

可以使用functor:

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;

这个解决方案是标准相关的,而不是编译器相关的。

或者您可以使用foostatic变量

所以我犹豫是否检查使用的最后一个参数的原因是,函数调用在一些非常紧密的内循环中,所以额外的比较和分支指令会很烦人,特别是在具有(或非常差的)分支预测的平台上。

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

#include <stdio.h>
int check;
void myfun(int num){
        printf("changing to %dn",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可以正确地执行此操作。如果有人想对这里的优化进行任何限制,我将非常感兴趣