【问题标题】:virtual function overhead when deterministic (c++)确定性时的虚函数开销(c ++)
【发布时间】:2016-02-10 17:31:14
【问题描述】:

我知道虚函数本质上是包含在 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;
}

【问题讨论】:

  • 值没有多态性,因此我不太明白你的意思 a)。
  • Foo* a = new Foo()Foo* b = new Foo() 没有区别。你想在这里说明什么?
  • 这些都是潜在可优化的,因为abc 的动态类型在编译时都是已知的。编译器是否真正执行优化是另一回事。正如 Yam Marcovic 所说,这是一个活跃的研究领域,因此最先进的技术在不断变化。
  • 您想知道编译器优化,但我想知道您的问题是什么以及您自己做了什么来回答它......

标签: c++ polymorphism


【解决方案1】:

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

在第二种情况下,它更复杂。一方面,它充其量是链接时间优化,因为对于特定的翻译单元,编译器不知道还有谁可能从该类继承。您遇到的另一个问题是共享库,它也可能在您的可执行文件不知情的情况下继承。

不过,一般来说,这是一种编译器优化,称为虚拟函数调用消除,或去虚拟化,在某种程度上是一个活跃的研究领域。有些编译器在某种程度上做到了这一点,有些则根本不这样做。

参见,在 GCC (g++) 中,-fdevirtualize-fdevirtualize-speculatively。这些名称暗示了质量保证水平。

【讨论】:

    【解决方案2】:

    在 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
    

    【讨论】:

    • 这是使用O3时的结果吗?
    • 我对组装几乎一无所知。这是(对于虚拟调用)对“函数指针”的调用吗?在汇编中看起来最像它?
    【解决方案3】:

    一般而言,您可以相信您的编译器优化器会做出正确的选择,当然这取决于优化设置。

    为了概念证明,这里使用不同情况的代码,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 提交的所有案例生成的here the assembly code,而无需优化。它被着色以帮助您查看每个 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-&gt;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() 不应该进一步派生的事实。但这不一定完成。

    【讨论】:

      猜你喜欢
      • 2010-12-10
      • 2011-03-09
      • 1970-01-01
      • 2011-10-09
      • 1970-01-01
      • 2015-07-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多