【问题标题】:How can I elide additional calls to an idempotent function?如何省略对幂等函数的额外调用?
【发布时间】:2014-08-22 21:17:23
【问题描述】:

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

【问题讨论】:

  • 假设您可以以某种方式告诉编译器为您执行此操作。如果不将最后使用的参数保存在全局变量中,您认为它将如何实现?您对这种技术的反对意见究竟是什么?在您看来,它“不是最理想的”是什么?
  • 类似属性((const))的东西?
  • 一个有副作用的函数不是const。
  • 这必须是一个相当繁重的函数来证明开销是合理的,但是您可以将数字存储在一个集合中,并有条件地执行它们......该集合在方法中可能是静态的......
  • 您可以在编译前通过uniq 运行该代码块(在去掉 cmets 之后)。

标签: c++ c gcc


【解决方案1】:

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

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

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

Idempotence, computer science meaning:

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

而 C++ 没有这个概念。

【讨论】:

  • 有趣。我知道 f(f(x))=f(x) 定义,但不知道“第一次调用后修改的状态保持不变”。定义。我会接受这个答案,即使它令人失望。
  • 有一个简单的方法可以启用优化:只需提供一个小的内联函数进行检查,如果需要,它将调用一个外部函数来完成繁重的工作。
【解决方案2】:

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

【讨论】:

  • 第一次调用函数怎么办?那么static 变量会有什么值?
  • 通常你设置static int result = -1或者一些无效的输入。 (我偶尔也写过这样的代码)
  • @MooingDuck 他们没有在每个函数调用中初始化?
  • @GingerPlusPlus 您可以简单地设置创建时的值。为了清楚起见,添加了这四个字符:) 不,它们是静态的。它们只被初始化一次。
【解决方案3】:

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

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

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

【讨论】:

    【解决方案4】:

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

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

    【讨论】:

    • Haskell 也解决了这个问题。附言它被称为共享,也就是散列consing。
    • @MasterMastic 也叫自动记忆。取决于上下文。
    【解决方案5】:

    你可以使用仿函数:

    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 变量。

    【讨论】:

      【解决方案6】:

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

      当我尝试它时,我决定看看 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 似乎可以正确执行此操作。如果有人想对这里的优化提出任何限制,我会很感兴趣

      【讨论】:

        猜你喜欢
        • 2018-07-29
        • 1970-01-01
        • 1970-01-01
        • 2022-12-20
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多