14

我有以下情况:


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

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

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

澄清:我意识到,除非您尝试并计时,否则您永远不会真正知道哪种方法可以提供更好的性能。但是,在这种情况下,我已经实现了方法 1,我想知道方法 2 是否至少在原则上更有效。看来它可以,现在我费心去实现它并尝试它是有意义的。

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

4

12 回答 12

12

你有多确定通过指针调用成员函数比直接调用它慢?你能测量出差异吗?

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

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

于 2008-09-22T04:18:45.237 回答
9

你可以这样写:

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. 该switch版本不提供任何性能提升。

于 2008-09-22T05:39:07.527 回答
2

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

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

于 2008-09-22T04:22:22.797 回答
2

如果你打算继续使用一个开关,这很好,那么你可能应该把逻辑放在一个辅助方法中,并从构造函数中调用 if 。或者,这是策略模式的经典案例。您可以创建一个名为 IFoo 的接口(或抽象类),它有一个带有 Foo 签名的方法。你会让构造函数接受一个 IFoo 实例(构造函数依赖注入,它实现了你想要的 foo 方法。你会有一个私有的 IFoo ,它会用这个构造函数设置,每次你想调用 Foo 你都会调用你的IFoo 的版本。

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

于 2008-09-22T04:25:15.737 回答
2

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

但是,如果您的代码只是一个简化示例,询问一般来说跳转表是否比 switch 语句更快,那么我的直觉会说跳转表更快,但您依赖于编译器的优化步骤。但是,如果性能真的是这样一个问题,永远不要依赖直觉——敲出一个测试程序并测试它,或者查看生成的汇编程序。

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

于 2008-09-22T07:01:26.173 回答
1

听起来您应该创建callFoo一个纯虚函数并创建一些A.

除非您真的需要速度,否则已经进行了广泛的分析和检测,并确定调用callFoo确实是瓶颈。你?

于 2008-09-22T04:19:42.833 回答
1

函数指针几乎总是比链式 if 更好。它们使代码更清晰,并且几乎总是更快(除非在它只能在两个函数之间进行选择并且总是被正确预测的情况下)。

于 2008-09-22T04:24:53.727 回答
1

我应该认为指针会更快。

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

当然,您应该同时测量两者。

于 2008-09-22T04:25:44.020 回答
1

仅在需要时进行优化

第一:大多数时候你很可能不在乎,差别会很小。首先确保优化此调用确实有意义。仅当您的测量结果表明调用开销确实花费了大量时间时,才继续对其进行优化(无耻插件 - 参见如何优化应用程序以使其更快?)如果优化不显着,则更喜欢可读性更高的代码。

间接调用成本取决于目标平台

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

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

一个问题:切换有时也可以实现为间接调用(使用表格),尤其是在许多可能值之间切换时。这种开关表现出与虚函数相同的错误预测。为了使这种优化可靠,对于最常见的情况,人们可能更喜欢使用 if 而不是 switch。

于 2008-09-22T09:20:39.317 回答
1

使用计时器来查看哪个更快。尽管除非这段代码一遍又一遍地重复,否则您不太可能注意到任何差异。

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

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

于 2008-09-22T13:30:35.347 回答
1

如果您只调用 callFoo() 一次,那么函数指针很可能会慢一些。如果您多次调用它,则函数指针很可能会快一点(因为它不需要继续通过开关)。

无论哪种方式,查看汇编代码以确定它正在做你认为它正在做的事情。

于 2008-09-22T13:38:14.410 回答
1

切换(甚至超过排序和索引)的一个经常被忽视的优势是,如果您知道在绝大多数情况下都使用特定值。订购开关很容易,以便首先检查最常见的开关。

附言。为了加强格雷格的回答,如果你关心速度 - 测量。当 CPU 具有预取/预测分支和流水线停顿等时,查看汇编程序无济于事

于 2008-09-22T13:48:04.400 回答