使用函数指针剪切 if 语句会更有效

Is cutting if statements by using function pointers going to be more efficient?

本文关键字:语句 有效 if 函数 指针      更新时间:2023-10-16

所以,有这个规则试图从高重复循环中提取if语句:

for( int i = 0 ; i < 10000 ; i++ )
{
    if( someModeSettingOn )  doThis( data[i] ) ;
    else  doThat( data[i] ) ;
}

他们说,最好把它分解,把if语句放在外面:

if( someModeSettingOn )
  for( int i = 0 ; i < 10000 ; i++ )
    doThis( data[i] ) ;
else
  for( int i = 0 ; i < 10000 ; i++ )
    doThat( data[i] ) ;      

(如果你说"嗬!不要自己优化!编译器会做到的!当然,优化程序可能会为您执行此操作。 但是在典型的C++胡说八道(我不同意他的所有观点,例如他对虚函数的态度)中,迈克·阿克顿说:"为什么要让编译器猜测你知道的东西? 对我来说,这些粘性中最好的一点。

那么为什么不使用函数指针呢?

FunctionPointer *fp ;
if( someModeSettingOn )  fp = func1 ;
else fp = func2 ;
for( int i = 0 ; i < 10000 ; i++ )
{
    fp( data[i] ) ;
}

函数指针是否存在某种隐藏开销? 它像调用直函数一样有效吗?

在这个例子中,不可能说哪种情况会更快。您需要在目标平台/编译器上分析此代码以估计它。

一般来说,在 99% 的情况下,这样的代码不需要优化。这是邪恶过早优化的例子。编写人类可读的代码,并在分析后仅在需要时对其进行优化。

不要猜测,测量

但是,如果我绝对必须猜测,我会说第三个变体(函数指针)会比第二个变体(if外部循环)慢,我怀疑它可能会更好地使用 CPU 的分支预测。

正如您已经注意到的,第一个变体可能等同于也可能不等同于第二个变体,具体取决于编译器的智能程度。

为什么要让编译器猜测你知道的东西?

因为您可能会使未来的维护者的代码复杂化,而不会为代码的用户提供任何切实的好处。这种变化闻起来很早的优化,只有在分析之后,我才会考虑除了明显的(if内部循环)实现之外的任何事情。

鉴于分析表明这是一个问题,那么作为猜测,我相信将if拉出循环会比函数指针更快,因为指针可能会添加编译器无法优化的间接级别。它还将降低编译器可以内联任何调用的可能性。

但是,我也会考虑使用抽象接口而不是循环中的if的替代设计。然后,每个数据对象已经知道要自动执行的操作。

我敢打赌第二个版本是循环外if/else最快的版本,前提是当我们在最广泛的编译器中绑定和测试它时,我会获得退款。 :-D我打赌这个赌注已经有相当多年了,手里拿着 VTune。

也就是说,如果我输了赌注,我实际上会很高兴。我认为现在许多编译器可以优化第一个版本以与第二个版本竞争是非常可行的,检测到您正在重复检查一个在循环内没有变化的变量,从而有效地提升分支发生在循环之外。

但是,我还没有遇到过优化器执行内联间接函数调用的类比等效项的情况......尽管如果存在优化器可以执行此操作的情况,那么您的绝对是最简单的,因为它将地址分配给要在通过函数指针调用这些函数的同一函数中调用的函数。如果优化器现在可以做到这一点,我会感到非常惊喜,特别是因为从可维护性的角度来看,我最喜欢你的第三个版本(例如,如果我们想添加新条件导致调用不同的函数,则最容易更改)。

尽管如此,如果它无法内联,那么函数指针解决方案将倾向于成本最高,不仅因为跳远和潜在的额外堆栈溢出等等,还因为优化器将缺乏信息 - 当它不知道将通过指针调用什么函数时,存在优化器障碍。此时,它不能再将所有这些信息合并到IR中,并最好地完成指令选择,寄存器分配等工作。间接函数调用的这种编译器设计方面并不经常讨论,但可能是间接调用函数最昂贵的部分。

不确定它是否符合"隐藏"的条件,但当然使用函数指针需要另一个级别的间接性。

编译器必须生成代码来取消引用指针,然后跳转到生成的地址,而不是直接跳转到常量地址的代码,以进行正常的函数调用。

您有三种情况:

如果在循环内

,函数指针在循环内取消引用,如果在循环外部。

在这三个中,没有编译器优化,第三个将是最好的。第一个执行条件,第二个在要运行的代码之上执行指针取消引用,而第三个只运行所需的内容。

如果你想优化自己,不要做函数指针版本!如果您不信任编译器进行优化,那么额外的间接寻址最终可能会花费您,并且将来意外中断要容易得多(在我看来)。

你必须测量哪个更快 - 但我非常怀疑函数指针答案会更快。检查标志概率在具有深度多个管道的现代处理器上具有零延迟。而函数指针可能会使编译器被迫执行实际的函数调用、推送寄存器等。

"为什么要让编译器猜测你知道的东西?"

你和编译器在编译时都知道一些事情 - 但处理器在运行时知道更多的事情 - 比如内部循环中是否有空管道。进行这种优化的日子已经超出了嵌入式系统和图形着色器的范围。

其他人都提出了非常有效的观点,最值得注意的是你必须衡量。我想补充三件事:

  1. 一个重要的方面是,使用函数指针通常会阻止内联,这可能会降低代码的性能。但这绝对取决于。尝试使用 godbolt 编译器资源管理器,看看生成的程序集:

    https://godbolt.org/g/85ZzpK

    请注意,当未定义doThisdoThat时,例如,跨DSO边界可能发生的情况,不会有太大区别。

  2. 第二点与分支预测有关。看看 https://danluu.com/branch-prediction/。它应该清楚地表明,您在此处拥有的代码实际上是分支预测器的理想情况,因此您可能不必费心。同样,像 perf 或 VTune 这样的优秀分析器会告诉您是否遭受分支错误预测的困扰。

  3. 最后,我至少见过一种情况,尽管有上述推理,但将条件从循环中吊起来会产生巨大的差异。这是一个紧密的数学循环,由于条件,它没有被自动矢量化。GCC 和 Clang 都可以输出有关哪些循环被矢量化的报告,或者为什么没有这样做。就我而言,条件确实是自动矢量化器的问题。不过,这是GCC 4.8,所以从那时起情况可能已经发生了变化。使用 Godbolt,很容易检查这是否是您的问题。同样,始终在目标计算机上进行测量并检查您是否受到影响。