Overhead of a virtual function with determinate (C ++)

I know that virtual functions are essentially pointers to the functions contained in the vtable, which makes polymorphic calls slower due to indirection, etc. But I'm interested in optimizing the compiler when the call is deterministic. Deterministic, I mean the following cases:

  • An object is a value, not a reference, so polymorphism does not exist:
struct Foo { virtual void DoSomething(){....} }; int main() { Foo myfoo; myfoo.DoSemthing(); return 0; } 
  1. Link to the class without children:
 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 a polymorphic call, but b 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; } 
+5
source share
3 answers

In the first case, it will not be a virtual call. The compiler issues a direct call to Foo::DoSomething() .

In the second case, it is more complicated. Firstly, this is the best optimization of connection time, since for a specific translation unit the compiler does not know who else can inherit this class. Another problem you are facing is shared libraries, which can also inherit without your executable knowing this.

In general, however, this is compiler optimization, known as the elimination of virtual functions, or devirtualization, and to some extent is an active area of ​​research. Some compilers do this to some extent, others do not do this at all.

See GCC (g ++), -fdevirtualize and -fdevirtualize-speculatively . Names give a hint of a guaranteed level of quality.

+9
source

In Visual Studio 2013, a virtual function call is not optimized, even if the behavior is deterministic.

For instance,

 #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; } 

The machine code for the virtual call is as follows:

 a->VirtualCall() 0001b 8b 01 mov eax, DWORD PTR [ecx] 0001d ff 10 call DWORD PTR [eax] 

The machine code for a regular call indicates that the function was built-in - the function call was not executed:

  a->RegularCall() 00 inc DWORD PTR _counter 
+2
source

In general, you can trust your compiler optimizer to make a good choice, depending on the optimization settings.

To prove the concept, here we use code that uses different cases, Foo and Bar , as you did:

 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; } 

You can find here the assembly code generated for all cases that you submitted using GCC 5.3.0 without optimization. It is colored to help you see the assembler code for each C ++ statement.

The first call will always be a direct call:

  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 

Without optimization, all other DoSomething() calls will use an indirect call. here's an example 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 

If you now set the -O2 optimization flag to the compiler options, you will see that most indirect calls are optimized when the compiler can predict the actual type of polymorphic pointer. In the above example, this would be:

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

When the compiler cannot safely predict the real type, it will use an indirect call. For example, if you have a function bar_factory() that returns a Bar pointer, the compiler cannot know whether it will return a pointer to a Bar object or a class object derived from Bar (which can be defined in another compilation unit and are not known here).

The only unexpected thing is the definition of a virtual function as a final override (the Tzar class in my example). Here you can expect the compiler to take advantage of the fact that DoSomething() should not get further results. But this is optional.

+1
source

Source: https://habr.com/ru/post/1242717/


All Articles