题
我有以下情况:
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具有预取/预测分支和流水线停顿等时,查看汇编器并没有帮助