静态调用虚函数时?

时间:2017-04-06 10:41:14

标签: c++ performance pointers polymorphism virtual-functions

直接从派生类指针调用虚函数与​​从基类指针调用同一派生类之间的性能差异是什么?

在派生指针的情况下,调用是statically bound还是动态绑定?我认为它将动态绑定,因为无法保证派生指针实际上并未指向另一个派生类。如果我直接通过值(而不是通过指针或引用)获得派生类,情况会改变吗?所以3个案例:

  1. 指向派生的基指针
  2. 派生指向派生的指针
  3. 按值派生
  4. 我关注性能,因为代码将在微控制器上运行。

    演示代码

    struct Base {
        // virtual destructor left out for brevity
        virtual void method() = 0;
    };
    
    struct Derived : public Base {
        // implementation here
        void method() {
        }
    }
    
    // ... in source file
    // call virtual method from base class pointer, guaranteed vtable lookup
    Base* base = new Derived;
    base->method();
    
    // call virtual method from derived class pointer, any difference?
    Derived* derived = new Derived;
    derived->method();
    
    // call virtual method from derived class value
    Derived derivedValue;
    derived.method();
    

4 个答案:

答案 0 :(得分:2)

  • 理论上,唯一有所作为的C ++语法是使用限定成员名称的成员函数调用。就您的类定义而言

    derived->Derived::method();
    

    此调用忽略对象的动态类型并直接转到Derived::method(),即它静态绑定。这只适用于调用类本身或其祖先类中声明的方法。

    其他所有内容都是常规的虚函数调用,它根据调用中使用的对象的动态类型进行解析,即它是动态绑定的。

  • 实际上,编译器将努力优化代码,并在编译时已知对象的动态类型的上下文中使用静态绑定调用替换动态绑定调用。例如

    Derived derivedValue;
    derivedValue.method();
    

    通常会在几乎所有现代编译器中生成静态绑定调用,即使语言规范没有为这种情况提供任何特殊处理。

    此外,直接从构造函数和析构函数进行的虚方法调用通常会编译为静态绑定调用。

    当然,智能编译器可能能够在更多种类的上下文中静态绑定调用。例如,两者

    Base* base = new Derived;
    base->method();
    

    Derived* derived = new Derived;
    derived->method();
    

    可以被编译器看作是容易允许静态绑定调用的琐碎情况。

答案 1 :(得分:1)

虚拟函数必须编译才能像虚拟调用它们一样工作。如果您的编译器将虚拟调用编译为静态调用,那么这是一个必须满足此as-if规则的优化。

由此可见,编译器必须能够证明所讨论对象的确切类型。并且有一些有效的方法可以做到这一点:

  • 如果编译器看到对象的创建(new表达式或从中获取地址的自动变量)并且可以证明该创建实际上是当前指针值的来源,这给了它所需的精确动态类型。您的所有示例都属于此类别。

  • 构造函数运行时,对象的类型正是包含正在运行的构造函数的类。因此,可以静态解析在构造函数中进行的任何虚函数调用。

  • 同样,在析构函数运行时,对象的类型正是包含正在运行的析构函数的类。同样,任何虚函数调用都可以静态解析。

Afaik,这些都是允许编译器将动态调度转换为静态调用的所有情况。

所有这些都是优化,但编译器可能决定执行运行时vtable查找。但优秀的优化编译器应该能够检测到所有这三种情况。

答案 2 :(得分:-1)

前两种情况之间应该没有区别,因为虚函数的想法总是调用实际的实现。抛弃编译器优化(理论上可以优化所有虚函数调用,如果你在同一个编译单元中构造对象,并且指针之间无法改变),第二个调用必须实现为间接(虚拟) )也可以调用,因为可能有第三个类继承Derived并实现该函数。我假设第三个调用不是虚拟的,因为编译器在编译时已知道实际类型。实际上你可以通过不将函数定义为虚函数来确保这一点,如果你知道你将永远直接对派生类进行调用。

对于在小型微控制器上运行的非常轻量级的代码,我建议避免将功能定义为虚拟。通常不需要运行时抽象。如果你编写一个库并需要某种抽象,你可以使用模板代替(这会给你一些编译时的抽象)。

至少在PC CPU上,我经常发现虚拟调用是您可以拥有的最昂贵的间接之一(可能因为分支预测更加困难)。有时人们也可以将间接转换为数据级别,例如你保留一个通用函数,它对不同的数据进行操作,这些数据是指向实际实现的指针。当然,这仅适用于某些特定情况。

答案 3 :(得分:-3)

在运行时。

但是:性能与什么相比?将虚函数调用与非虚函数调用进行比较无效。您需要将它与非虚函数调用加上/var/log/shiny-server/if,间接或其他提供相同功能的方法进行比较。如果该功能没有体现在实现之间的选择,即不需要是虚拟的,则不要使其成为虚拟。