我有以下情况:


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 的值保存在构造函数的数据成员中,并使用 callFoo()中的 switch 决定要拨打哪个foo。或者,我可以在构造函数中使用 switch 来保存指向 callFoo()中要调用的右 fooN()的指针。

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

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

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

有帮助吗?

解决方案

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

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

更多信息:有一篇很好的文章会员功能指针和最快可能的C ++代表详细介绍了成员函数指针的实现。

其他提示

你可以这样写:

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

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

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

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

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

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

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

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

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

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

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

我认为指针会更快。

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

当然,你应该测量两者。

仅在需要时进行优化

第一:大多数时候你很可能不在乎,差异会非常小。确保首先优化此调用才有意义。只有当您的测量结果显示在呼叫开销中花费了大量时间时,才能继续优化它(无耻的插件 - Cf。如何优化应用程序以使其更快?)如果优化不重要,则更喜欢更易读的代码。

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

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

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

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

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

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

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

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

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

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

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

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top