代码之家  ›  专栏  ›  技术社区  ›  Dima

使用指向成员函数的指针与使用开关的成本是多少?

  •  12
  • Dima  · 技术社区  · 16 年前

    我有以下情况:

    
    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() 决定哪个比赛场地打电话。或者,我可以使用 转换 在构造函数中保存指向右侧的指针 fooN() 被召入 CALFULL() .

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

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

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

    12 回复  |  直到 13 年前
        1
  •  11
  •   Greg Hewgill    16 年前

    通过指针调用成员函数比直接调用要慢多少?你能测量出差别吗?

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

    更多信息:有一篇优秀的文章 Member Function Pointers and the Fastest Possible C++ Delegates 对成员函数指针的实现有很深的了解。

        2
  •  8
  •   mskfisher KeithS    13 年前

    你可以这样写:

    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 . 这个 转换 版本不提供任何性能提升。

        3
  •  2
  •   DocMax    16 年前

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

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

        4
  •  2
  •   Charles Graham    16 年前

    如果您要继续使用开关,这是非常好的,那么您可能应该将逻辑放在一个助手方法中,并从构造函数调用if。或者,这是 Strategy Pattern .您可以创建一个名为ifoo的接口(或抽象类),它有一个带有foo签名的方法。您可以让构造函数接受ifoo(构造函数)的一个实例 Dependancy Injection 实现了您想要的foo方法。您将拥有一个使用此构造函数设置的私有ifoo,并且每次您想要调用foo时,都会调用ifoo的版本。

    注意:我从大学起就没有使用C++,所以我的语言可能在这里,大多数的OO语言都有这样的想法。

        5
  •  2
  •   Greg Whitfield    16 年前

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

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

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

        6
  •  1
  •   Thomas    16 年前

    听起来你应该 callFoo 纯虚函数并创建 A .

    除非您真的需要速度,否则已经进行了大量的分析和检测,并确定调用 卡洛福 是真正的瓶颈。有你?

        7
  •  1
  •   Dark Shikari    16 年前

    函数指针几乎总是比链接的IFS好。它们可以生成更清晰的代码,而且几乎总是更快的(除非在两个函数之间只有一个选择并且总是正确预测的情况下)。

        8
  •  1
  •   community wiki 2 revs TraumaPony    16 年前

    我想指针会更快。

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

    当然,你应该测量两者。

        9
  •  1
  •   Community WizardZ    7 年前

    仅在需要时优化

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

    间接呼叫成本取决于目标平台

    一旦确定了应用低级优化是值得的,那么现在就是了解目标平台的时候了。你在这里可以避免的成本是分行预测失误罚款。在现代x86/x64 CPU上,这种预测失误可能非常小(大多数情况下,它们可以很好地预测间接调用),但当针对PowerPC或其他RISC平台时,通常根本无法预测间接调用/跳转,避免它们会导致显著的性能提升。也见 Virtual call cost depends on platform .

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

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

        10
  •  1
  •   community wiki Dynite    16 年前

    使用计时器查看哪个更快。尽管除非这段代码会反复出现,否则您不太可能注意到任何不同。

    如果您正在运行来自构造函数的代码,请确保如果构造失败,您不会泄漏内存。

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

        11
  •  1
  •   Greg Rogers    16 年前

    如果只调用一次callfoo(),则 最可能 函数指针的速度将变慢一点。如果你打了很多次电话 最可能 函数指针的速度会更快一些(因为它不需要一直通过开关)。

    无论哪种方法,都要查看组装好的代码,以确定它在做您认为它在做的事情。

        12
  •  1
  •   Martin Beckett    16 年前

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

    另外,为了加强格雷格的回答,如果你关心速度测量。 当CPU有预取/预测性分支和管道暂停等情况时,查看汇编程序并没有帮助。