7

因此,有这条规则可以尝试将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++ 废话中(我不同意他的所有观点,例如他对虚函数的态度)Mike Acton 说:“为什么要让编译器猜测你知道的东西?对我来说,这几乎是这些粘性的最佳点。

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

FunctionPointer *fp ;
if( someModeSettingOn )  fp = func1 ;
else fp = func2 ;

for( int i = 0 ; i < 10000 ; i++ )
{
    fp( data[i] ) ;
}

函数指针是否存在某种隐藏开销?调用直接函数是否有效?

4

8 回答 8

8

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

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

于 2012-06-19T14:55:09.473 回答
6

不要猜测,测量

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

正如您已经指出的,第一个变体可能与第二个变体等效,也可能不等效,具体取决于编译器的智能程度。

于 2012-06-19T15:07:13.177 回答
6

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

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

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

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

于 2012-06-19T15:31:55.220 回答
2

我的赌注是第二个版本if/else在循环外是最快的,前提是当我们在最广泛的编译器中捆绑和测试它时我会得到退款。:-DI 在 VTune 的支持下用了很多年的时间做这个赌注。

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

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

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

于 2018-01-06T12:47:04.493 回答
1

你有三种情况:

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

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

如果您想优化自己,请不要使用函数指针版本!如果您不相信编译器会进行优化,那么额外的间接性最终可能会让您付出代价,并且将来意外中断会容易得多(在我看来)。

于 2012-06-19T15:09:12.207 回答
1

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

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

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

于 2012-06-19T16:43:07.247 回答
1

不确定它是否符合“隐藏”的条件,但当然使用函数指针需要多一层间接。

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

于 2012-06-19T14:57:48.517 回答
1

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

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

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

    请注意,当doThisdoThat未定义时,例如可能发生在 DSO 边界上,不会有太大差异。

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

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

于 2018-01-06T13:45:28.667 回答