我有以下情况:
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,因为美学原因。我想我正在寻找实现它的理由。 :)
答案 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具有预取/预测分支和流水线停顿等时,查看汇编器并没有帮助