繁体   English   中英

确定性时的虚函数开销(c ++)

[英]virtual function overhead when deterministic (c++)

我知道虚函数本质上是包含在vtable上的函数指针,这使得多态调用由于间接等而变慢。但是当调用是确定性的时候我想知道编译器优化。 确定性,我指的是以下情况:

  1. 对象是值而不是引用,因此不存在多态:
struct Foo
{
    virtual void DoSomething(){....}
};

int main()
{
    Foo myfoo;
    myfoo.DoSemthing();
    return 0;
}
  1. 参考是一个没有孩子的类:
struct Foo
{
    virtual void DoSomething();
};
struct Bar : public Foo
{
   virtual void DoSomething();
};

int main()
{
    Foo* a = new Foo();
    a->DoSomething(); //Overhead ? a doesn't seem to be able to change nature.

    Foo* b = new Bar();
    b->DoSomething(); //Overhead ? It's a polymorphic call, but b's nature is deterministic.

    Bar* c = new Bar();
    c->DoSomething(); //Overhead ? It is NOT possible to have an other version of the method than Bar::DoSomething
    return 0;
}

在第一种情况下,这不是虚拟呼叫。 编译器将直接向Foo::DoSomething()发出调用。

在第二种情况下,它更复杂。 首先,它最好是链接时间优化,因为对于特定的转换单元,编译器不知道还有谁可能从该类继承。 您遇到的另一个问题是共享库,如果没有您的可执行文件知道任何相关内容,它们也可能继承。

但总的来说,这种编译器优化称为虚函数调用消除虚拟化 ,并且在某种程度上是一个活跃的研究领域。 有些编译器在某种程度上做到了,有些编译器根本没做。

请参阅GCC(g ++)中的-fdevirtualize-fdevirtualize-speculatively 这些名字暗示着保证的质量水平。

在Visual Studio 2013中,即使行为是确定性的,也不会优化虚函数调用。

例如,

#include <iostream>

static int counter = 0;

struct Foo
{
    virtual void VirtualCall() { ++counter; }
    void RegularCall() { ++counter; }
};

int main()
{
    Foo* a = new Foo();
    a->VirtualCall(); //Overhead ? a doesn't seem to be able to change nature.
    a->RegularCall();
    std::cout << counter;
    return 0;
}

虚拟呼叫的机器代码如下所示:

a->VirtualCall()

  0001b 8b 01        mov     eax, DWORD PTR [ecx]
  0001d ff 10        call    DWORD PTR [eax]

常规调用的机器代码显示函数是内联的 - 没有函数调用:

 a->RegularCall()

  00         inc     DWORD PTR _counter

通常,您可以信任编译器优化器,以便做出正确的选择,具体取决于优化设置。

为了概念证明,这里使用不同情况的代码, FooBar被定义为你所做的:

struct Tzar : public Foo
{
   void DoSomething() override final;  // this is a virtual than can't be overriden further
};

Foo* factory ();  
Bar* bar_factory(); 
Tzar* tsar_factory(); 

int main()
{
    Foo myfoo;
    myfoo.DoSomething();  // this is a direct call

    Foo* a = new Foo();
    a->DoSomething();  //Overhead only without optimisation: a is clearly a Foo, so Foo::DoSomething(). 

    Foo* b = new Bar();
    b->DoSomething(); //Overhead only without optimisation:  b is clearly a Bar, so Bar::DoSomething().

    Bar* c = new Bar();
    c->DoSomething(); //Overhead only without optimisation: c is clearly a Bar, so Bar::DoSomething

    Foo* d = factory(); 
    d->DoSomething();  // Overhead required:  we don't know the type of d, unless global optimisation could predict it

    a = d; 
    a->DoSomething();  //the unknown propagates to a, so now this call is indirect 

    Foo*e = bar_factory(); 
    e->DoSomething();  // Overhead required:  we don't know the type of e: could be a Bar or a furhter derivate unknown in this compilation unit

    Foo*f = tsar_factory(); 
    f->DoSomething();  // Overhead could be optimised away : we don't know the type of f, but f::DoSomething() can't be overriden further
                       // but currently it isn't

  return 0;
}

您可以在此处找到为使用GCC 5.3.0提交的所有案例生成的汇编代码 ,无需优化。 它的颜色可以帮助您查看每个C ++语句的汇编代码。

第一个电话永远是直接电话:

    lea     rax, [rbp-80]         ;  take the object pointer from the stack
    mov     rdi, rax              ;  set the this pointer of the invoking object
    call    Foo::DoSomething()    ; direct call to the function

如果没有优化, DoSomething()所有其他调用都将使用间接调用。 这里是b->DoSomething()的例子:

    mov     rax, QWORD PTR [rbp-32]
    mov     rax, QWORD PTR [rax]
    mov     rax, QWORD PTR [rax]  ; load the function call from the vtable
    mov     rdx, QWORD PTR [rbp-32]
    mov     rdi, rax              ;  set the this pointer of the invoking object
    call    rax                   ; indirect call via register 

如果现在在编译器选项中设置优化标志-O2,那么当编译器可以预测多态指针的实际类型时,您将看到大多数间接调用都被优化掉了。 在上面的例子中,它将是:

    mov     rdi, rax            ;  set the this pointer of the invoking object
    call    Bar::DoSomething()  ; direct call !! 

当编译器无法安全地预测实际类型时,它将使用间接调用。 例如,如果你有一个函数bar_factory() ,它返回一个Bar指针,编译器就不知道它是否会返回一个指向Bar对象的指针,或者是一个从Bar派生的类的对象(可以在另一个编译单元,这里不知道)。

唯一意想不到的一点是,当您将虚函数定义为最终覆盖时(在我的示例中为Tzar类)。 在这里,您可以期望编译器将利用DoSomething()不应该进一步派生的事实。 但它不一定完成。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM