使用函数指针来减少if语句是否会更有效率?

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] ) ;
}

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


12
你应该根据自己的情况进行性能测试并观察结果。分支预测已经大大降低了使用频繁的“if”语句所带来的开销。 - Mark Ransom
8个回答

7
在这个例子中,无法确定哪种情况更快。您需要在目标平台/编译器上对此代码进行分析以估算它。
一般来说,在99%的情况下,这样的代码不需要优化。这是早期恶意优化的例子。 编写易读的代码,并仅在分析后需要时进行优化。

6

不要猜测,量化

但是,如果我非常有必要去猜测,我会说第三个变体(函数指针)比第二个变体(if在循环外)更慢,因为我怀疑它可能与CPU的分支预测更好的玩耍。

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


6
为什么让编译器去猜你已知的东西呢?因为你可能会为未来的维护者增加代码复杂性,而对你代码的用户没有提供实际的好处。这个更改非常像过早优化,只有在进行分析之后才会考虑其他选择(if循环中的实现)。如果分析显示存在问题,那么我认为将if循环之外的语句提取出来比使用函数指针更快,因为指针可能会增加编译器无法优化的间接层级,同时也会减少编译器内联调用的可能性。但是,我也会考虑使用抽象接口的替代设计,而不是在循环中使用if语句。这样每个数据对象都可以自动地知道该做什么。

1
不仅指针的间接寻址会影响性能,函数调用本身也可能会阻止一些优化。 - Mark Ransom

2
我的赌注是第二个版本会是最快的,因为if/else在循环外部,前提是我们在尽可能多的编译器上测试并取得退款。:-D 我拥有相当多年的VTune经验。
话虽如此,如果我输了这个赌注,我也会很高兴。我认为现在许多编译器都可以优化第一个版本,使其与第二个版本相媲美,检测到您在循环内重复检查一个不会改变的变量,因此有效地将分支提升到循环外部。
然而,我还没有遇到过一种情况,即优化程序执行间接函数调用的类比等效操作...但如果有一种情况,优化程序肯定会很容易,因为它将函数的地址分配给要在同一函数中通过函数指针调用这些函数的函数。如果现在的优化程序能够做到这一点,我会非常惊喜,特别是因为从可维护性的角度来看,我最喜欢您的第三个版本(如果我们想添加导致调用不同函数的新条件,例如)。
但是,如果无法进行内联,则函数指针解决方案很可能是最昂贵的,不仅因为存在长跳转和潜在的额外堆栈溢出等问题,而且因为优化程序将缺少信息——当它不知道通过指针将调用哪个函数时,存在优化器障碍。此时,它无法将所有这些信息合并到IR中,并在指令选择、寄存器分配等方面做出最佳工作。间接函数调用的这种编译器设计方面并没有被经常讨论,但可能是调用间接函数最昂贵的部分。

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,检查这是否对您有影响非常容易。同样,始终在目标机器上进行测量并检查是否受到影响。


1

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

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


1
“直接跳转到常量地址的代码,用于普通函数调用”,这是普通函数调用的最坏情况。它可能会被内联。 - Steve Jessop
1
请注意,使用函数指针并不一定会有“更多的间接层级”。这是某种形式的call <register>指令,与某种形式的call <constant>函数(由动态链接器固定)或者可能是set <register> <constant>然后call <register>相比。我认为通常不会说x + yx + 12345相比涉及到了额外的间接性层级。如果东西被溢出到堆栈中,那么使用变量可能需要额外的工作,但是函数指针通常较慢的真正原因并不是这个。 - Steve Jessop

1

您有三个情况:

如果在循环内部,函数指针在循环内部解引用,如果在循环外部,则不需要编译器优化。

在这三种情况中,没有编译器优化的情况下,第三种情况是最好的。第一种情况进行条件检查,第二种情况在运行代码之上进行指针解引用,而第三种情况只运行您想要的代码。

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


如果您将三个条件与问题排列方式相同,您的答案会更有说服力。我不同意即使没有优化,“if”在循环外部一定比内部更快。然而,我同意它不太可能更慢。 - Mark Ransom
@MarkRansom - 有趣。你认为什么情况下它不会变慢?我能想到的唯一一件事是如果CPU正在进行分支预测,并且在循环过程中你从未失去CPU。 - Michael Kohne
1
不仅是分支预测 - 许多循环都受到内存带宽的限制,因此优化每个周期并不能真正帮助。哦,我多么怀念那些你可以查看指令并准确知道需要多少个时钟周期的日子!好吧,也许不是。 - Mark Ransom

1

你需要测量哪个更快 - 但我非常怀疑函数指针的答案会更快。在现代处理器上,检查标志可能具有零延迟和深度多管道。而函数指针将使编译器强制执行实际的函数调用,推动寄存器等。

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

你和编译器在编译时都知道一些东西 - 但处理器在运行时知道更多的东西 - 比如那个内部循环中是否有空管道。除了嵌入式系统和图形着色器外,进行这种优化的日子已经过去了。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接