使用成员函数指针与交换机的成本是多少?

时间:2008-09-22 04:15:52

标签: c++ function-pointers

我有以下情况:


class A
{
public:
    A(int whichFoo);
    int foo1();
    int foo2();
    int foo3();
    int callFoo(); // cals one of the foo's depending on the value of whichFoo
};

在我当前的实现中,我将whichFoo的值保存在构造函数的数据成员中,并使用switch中的callFoo()来决定调用哪个foo。或者,我可以在构造函数中使用switch来保存指向fooN()中要调用的右callFoo()的指针。

我的问题是,如果A类的对象只构造一次,而callFoo()被调用了很多次,那么哪种方式更有效。所以在第一种情况下我们有多个switch语句的执行,而在第二种情况下只有一个开关,并且使用指向它的指针多次调用成员函数。我知道使用指针调用成员函数比直接调用它要慢。有人知道这个开销是否高于或低于switch的成本吗?

澄清:我意识到你从来没有真正知道哪种方法可以提供更好的性能,直到你尝试并计时。但是,在这种情况下,我已经实施了方法1,并且我想知道方法2是否可以至少在原则上更有效。它似乎可以,现在我有理由去实现它并尝试它。

哦,我也更喜欢方法2,因为美学原因。我想我正在寻找实现它的理由。 :)

12 个答案:

答案 0 :(得分:11)

你是否确定通过指针调用成员函数比直接调用它更慢?你能衡量一下差异吗?

一般来说,在进行绩效评估时,不应该依赖自己的直觉。坐下来使用编译器和计时功能,实际上测量不同的选择。你可能会感到惊讶!

更多信息:有一篇很好的文章Member Function Pointers and the Fastest Possible C++ Delegates,它详细介绍了成员函数指针的实现。

答案 1 :(得分:8)

你可以这样写:

class Foo {
public:
  Foo() {
    calls[0] = &Foo::call0;
    calls[1] = &Foo::call1;
    calls[2] = &Foo::call2;
    calls[3] = &Foo::call3;
  }
  void call(int number, int arg) {
    assert(number < 4);
    (this->*(calls[number]))(arg);
  }
  void call0(int arg) {
    cout<<"call0("<<arg<<")\n";
  }
  void call1(int arg) {
    cout<<"call1("<<arg<<")\n";
  }
  void call2(int arg) {
    cout<<"call2("<<arg<<")\n";
  }
  void call3(int arg) {
    cout<<"call3("<<arg<<")\n";
  }
private:
  FooCall calls[4];
};

实际函数指针的计算是线性且快速的:

  (this->*(calls[number]))(arg);
004142E7  mov         esi,esp 
004142E9  mov         eax,dword ptr [arg] 
004142EC  push        eax  
004142ED  mov         edx,dword ptr [number] 
004142F0  mov         eax,dword ptr [this] 
004142F3  mov         ecx,dword ptr [this] 
004142F6  mov         edx,dword ptr [eax+edx*4] 
004142F9  call        edx 

请注意,您甚至不必在构造函数中修复实际的函数编号。

我已将此代码与switch生成的asm进行了比较。 switch版本不会提高性能。

答案 2 :(得分:2)

回答问题:在最细粒度的级别,指向成员函数的指针将表现得更好。

解决这个未提出的问题:“更好”在这里意味着什么?在大多数情况下,我认为差异可以忽略不计。然而,根据它所做的课程,差异可能很大。在担心差异之前进行性能测试显然是正确的第一步。

答案 3 :(得分:2)

如果你要继续使用一个非常好的开关,那么你可能应该把逻辑放在一个帮助器方法中并从构造函数中调用。或者,这是Strategy Pattern的经典案例。您可以创建一个名为IFoo的接口(或抽象类),它有一个带有Foo签名的方法。你可以让构造函数接受一个实现你想要的foo方法的IFoo实例(构造函数Dependancy Injection。你将拥有一个私有的IFoo,可以用这个构造函数设置,每次你想调用Foo你会打电话给你的IFoo版本。

注意:我从大学开始就没有使用过C ++,所以我的术语可能就在这里,对大多数OO语言都有一般的想法。

答案 4 :(得分:2)

如果您的示例是真实代码,那么我认为您应该重新审视您的课程设计。将值传递给构造函数,并使用它来改变行为实际上等同于创建子类。考虑重构以使其更明确。这样做的结果是你的代码最终会使用一个函数指针(所有虚拟方法实际上都是跳转表中的函数指针)。

但是,如果你的代码只是一个简单的例子来询问跳转表是否比switch语句更快,那么我的直觉会说跳转表更快,但你依赖于编译器的优化步骤。但是,如果性能确实是一个问题,那就不要依赖直觉 - 敲定测试程序并对其进行测试,或者查看生成的汇编程序。

有一点可以肯定,switch语句永远不会比跳转表慢。原因是编译器的优化器能做的最好的事情就是将一系列条件测试(即一个开关)转换成一个跳转表。因此,如果您真的想确定,请将编译器从决策过程中取出并使用跳转表。

答案 5 :(得分:1)

听起来你应该让callFoo成为一个纯虚函数并创建一些A的子类。

除非你真的需要速度,否则进行了大量的分析和检测,并确定对callFoo的调用确实是瓶颈。你呢?

答案 6 :(得分:1)

函数指针几乎总是比chained-ifs更好。它们使代码更清晰,并且几乎总是更快(除非它只能在两个函数之间进行选择并始终正确预测)。

答案 7 :(得分:1)

我认为指针会更快。

现代CPU预取指令;错误预测的分支刷新缓存,这意味着它在重新填充缓存时停止。指针不会这样做。

当然,你应该测量两者。

答案 8 :(得分:1)

仅在需要时进行优化

第一:大多数时候你很可能不在乎,差异会非常小。确保首先优化此调用才有意义。只有当您的测量显示在呼叫开销中花费了大量时间时,才能继续优化它(无耻插件 - 参见How to optimize an application to make it faster?)如果优化不重要,则更喜欢更易读的代码。

间接通话费用取决于目标平台

一旦确定应用低级优化是值得的,那么就是了解目标平台的时候了。您可以避免的成本是分支错误预测惩罚。在现代的x86 / x64 CPU上,这种误预测可能非常小(他们可以在大多数情况下很好地预测间接调用),但是当针对PowerPC或其他RISC平台时,通常根本不会预测间接调用/跳转并避免它们可以带来显着的性能提升。另请参阅Virtual call cost depends on platform

编译器也可以使用跳转表来实现切换

一个问题:Switch有时可以实现为间接调用(使用表),尤其是在许多可能的值之间切换时。这种开关表现出与虚拟功能相同的误预测。为了使这种优化可靠,人们可能更愿意使用if而不是switch来处理最常见的情况。

答案 9 :(得分:1)

使用计时器查看哪个更快。虽然除非这段代码一遍又一遍,否则你不太可能注意到任何差异。

请确保如果从构造函数运行代码,如果构造失败,则不会泄漏内存。

这种技术在Symbian OS中大量使用: http://www.titu.jyu.fi/modpa/Patterns/pattern-TwoPhaseConstruction.html

答案 10 :(得分:1)

如果你只调用一次callFoo(),那么很可能,函数指针的速度会慢一些。如果你多次调用它很可能,那么函数指针的速度会快得多(因为它不需要继续通过交换机)。

无论哪种方式,都要查看汇编的代码,以确定它是否正在按照您的想法进行操作。

答案 11 :(得分:1)

切换(甚至是排序和索引)的一个经常被忽视的优点是,如果您知道在绝大多数情况下使用了特定值。 订购交换机很容易,因此首先要检查最常见的交换机。

PS。如果你关心速度测量,要加强格雷格的答案。 当CPU具有预取/预测分支和流水线停顿等时,查看汇编器并没有帮助

相关问题