【问题标题】:If-statement vs function pointerIf 语句与函数指针
【发布时间】:2014-01-21 06:21:54
【问题描述】:

目标是改变事件循环中的行为,具体取决于复选框是打开还是关闭。我能想到的最简单的方法就是在每次运行循环时测试复选框状态。

// if-statement

void action() { /* ... */ }


void someLoop() {

  if (checkboxTrue) {
    action();
  }
  // ... other stuff

}

如果使用函数指针,代码会更高效、更简洁还是以其他方式更好?像这样:

// function pointer

void action() { /* ... */ }
void empty() {}
void (*actionPtr)();


void checkboxChanged(int val) {

  if (val == 1)
    actionPtr = &realAction;
  else
    actionPtr = ∅

}

void someLoop() {

  (*actionPtr)();
  // ... other stuff

}

【问题讨论】:

  • 没有。简单就好。如果没有性能问题,请始终选择可读性而不是性能。
  • 我会采用第一种方式。它更短,并说明了它的含义。对于如此无关紧要的事情,性能不是问题。
  • 您可能会获得相反的效果。可以预测分支并内联函数调用。但是,通过函数指针调用总是会产生间接成本。
  • 除了可能调用不确定函数指针的明显错误外,我发现很少使用第二种方法根本。如果检查复选框的状态确实是您应用程序盔甲中的一个性能问题,我很难想象它是多么简单。无论如何,个人资料,个人资料,个人资料......
  • 这里我不同意大家的看法,我认为函数指针总体上会更快。不过差别很小。

标签: c performance if-statement function-pointers data-oriented-design


【解决方案1】:

如果您只想在一个中心位置检查这些内容,我建议尽可能使用switch,否则使用if/else

首先,在需要时扩展和添加新案例实际上更容易,而不是编写新函数、检查事物和使用函数指针。如果您想在多个地方检查条件,函数指针解决方案将变得更容易扩展。

但其次(但不那么重要),它往往会更有效率,有时甚至相当有效,而且其方式不仅仅是比较动态调度与本地分支/跳转的成本。这是因为您为优化器提供了更多信息。

我曾经发现一个案例,其中 GCC 5.3 设法将一组复杂的 switch cases 压缩到每个案例中涉及多个函数调用的单个无分支查找表,该查找表将哪些数据加载到寄存器中。我希望有一个跳转表,但它跳过了整个跳转表,只是弄清楚了每种情况需要加载哪些数据而根本没有分支。

我印象非常深刻,因为我什至没有意识到所有这些 cases 调用函数可以归结为从 LUT 加载数据。但我很确定如果涉及间接函数调用,它就无法像这样压缩我的代码。它们最终会成为优化障碍,除非优化器非常聪明,以至于它可以为每种可能的函数指针类型生成所有代码路径,同时确定哪些函数将通过给定的 FP 调用。

【讨论】:

    【解决方案2】:

    恕我直言,对于调用具有许多参数的例程来说,指针更简洁:

    int good(int a, int b, int c, char *d) { ... }
    int bad (int a, int b, int c, char *d) { ... }
    int ugly(int a, int b, int c, char *d) { ... }
    

    直接调用:

    if (is_good) {
       good(1,2,3,"fire!");
    } else if (is_bad) {
       bad(1,2,3,"fire!");
    } else {
       ugly(1,2,3,"fire!");
    }
    

    间接调用:

    if (is_good) {
       f = good;
    } else if (is_bad) {
       f = bad;
    } else {
       f = ugly;
    }
    
    ...
    
    f(1,2,3,"fire!");
    

    【讨论】:

      【解决方案3】:
      1. 一个间接函数调用比一个 if 条件更昂贵。

      2. 如果条件比间接函数调用更昂贵。

      3. 此时担心速度是没有意义的:
        您正在等待用户的延迟,并且您正在处理他可以查看的内容(即不会有大量的复选框)。像这样在细节级别上优化每秒执行次数少于一百万次的代码绝对没有意义。

      所以,我的建议是:在编写用户界面时不要担心if 或函数调用的成本。只在耗时的算法中考虑这些东西。

      但是,如果您发现您确实在内部循环中使用了复杂的 if/else 阶梯和/或 switch 语句,您可以通过用间接函数调用替换它们来优化。


      编辑:
      你说你每秒有 600 次检查。假设您只有一个 if 案例要处理(if 更快的情况),通过不使用函数指针间接,您每秒“节省”大约 6 微秒,这是运行时间的 0.0006%。绝对不值得努力...

      【讨论】:

      • 几个 if 条件可能仍然比一个间接调用更快。在最近的 Intel CPU 上,一个间接调用在性能方面相当于大约 3 到 4 个 (if) 分支。
      • @VladLazarenko 仅当预测正确时。一个函数调用需要 10 到 20 个周期 afaik,一次缓存内存访问是 4 个周期延迟,间接调用需要 14 到 24 个周期,忽略了预加载函数指针的可能性,这会使这个数字再次下降。由于分支预测错误导致的两次管道刷新应该至少一样昂贵。
      【解决方案4】:

      没有条件分支显然会节省一些时间,当然它只是围绕一个分支分支,所以你会发生管道刷新,这可能是两次刷新而不是一次(当然排除处理器优化)加上进行比较的额外代码。

      extern void fun0 ( unsigned int );
      extern void fun1 ( unsigned int );
      
      void (*fun(unsigned int));
      
      
      void dofun0 ( unsigned int x, unsigned int y )
      {
          if(x) fun0(y);
          else  fun1(y);
      }
      
      void dofun ( unsigned int y )
      {
          fun(y);
      }
      

      给出这样的例子

      Disassembly of section .text:
      
      00000000 <dofun0>:
         0:   e3500000    cmp r0, #0
         4:   e92d4008    push    {r3, lr}
         8:   e1a00001    mov r0, r1
         c:   1a000002    bne 1c <dofun0+0x1c>
        10:   ebfffffe    bl  0 <fun1>
        14:   e8bd4008    pop {r3, lr}
        18:   e12fff1e    bx  lr
        1c:   ebfffffe    bl  0 <fun0>
        20:   e8bd4008    pop {r3, lr}
        24:   e12fff1e    bx  lr
      
      00000028 <dofun>:
        28:   e92d4008    push    {r3, lr}
        2c:   ebfffffe    bl  0 <fun>
        30:   e8bd4008    pop {r3, lr}
        34:   e12fff1e    bx  lr
      

      如果您仔细设计测试,您应该能够衡量这种性能差异。这将是一个非常小的差异,但绝对可以衡量。

      【讨论】:

      • 是的。但是如果 fun0 和 fun1 对编译器可见,它可以选择内联它们,这再次改变了故事。
      • 你是绝对正确的,根据指令集和其他因素,各种事情都可能发生。我假设函数指针是专门为避免 switch 或 if-then-else 树而发明的(出于各种原因),但这是另一个话题。因此,您可能会得到相等的东西,或者 if-then-else 会稍微慢一些。用精心设计的代码来演示很容易,速度较慢的情况。同样,演示“没有区别代码”很容易,但我将把它留给读者。
      • 很可能会出现极端情况或少数情况,if-then-else 会产生更快的代码。
      【解决方案5】:

      您的函数指针实现在 IMO 中根本不清楚。那里发生了什么并不明显,所以你应该避免这种情况。

      也许另一种选择是将检查/操作函数指针放入数组中。并在循环中检查它。您有一个干净的 someLoop 功能,并且您的检查/操作(“业务逻辑”)在此数组中。

      【讨论】:

        【解决方案6】:

        要获得准确的结果,需要时间测量,但是:
        我确信 if 的开销比完整的函数调用要少。
        ->如果更快

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2021-11-14
          • 2020-12-30
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-02-15
          • 1970-01-01
          相关资源
          最近更新 更多