Virtual functions are by definition slower than their non-virtual counterparts; we wanted to measure the performance gains from inlining; using virtual functions would make the difference even more pronounced. In this particular example, CLANG 10 compiler inlined the functions and unrolled the test loop two times.
Virtual functions are slow when you have a cache miss looking them up. As we'll see through benchmarks, they can be very slow. They can also be very fast when used carefully — to the point where it's impossible to measure the overhead.
(In more typical scenarios, you can expect that inlining a small function into a hot inner loop might reduce execution time by up to about an order of magnitude.) So in a worst case scenario, a function pointer call is arbitrarily slower than a direct function call, but this is misleading.
On that architecture, a virtual function call costs 7 nanoseconds longer than a direct (non-virtual) function call. So, not really worth worrying about the cost unless the function is something like a trivial Get()/Set() accessor, in which anything other than inline is kind of wasteful.
I'd say most of the C++ implementations work similar to this (and probably the first implementations, that compiled into C, produced code like this):
struct ClassVTABLE {
void (* virtuamethod1)(Class *this);
void (* virtuamethod2)(Class *this, int arg);
};
struct Class {
ClassVTABLE *vtable;
};
Then, given an instance Class x
, calling the method virtualmethod1
for it is like x.vtable->virtualmethod1(&x)
, thus one extra dereference, 1 indexed lookup from the vtable
, and one extra argument (= this
) pushed onto the stack / passed in registers.
However the compiler probably can optimize repeated method calls on an instance within a function: since an instance Class x
cannot change its class after it is constructed, the compiler can consider the whole x.vtable->virtualmethod1
as a common sub-expression, and move it out of loops. Thus in this case the repeated virtual method calls within a single function would be equivalent in speed to calling a function via a simple function pointer.
Are C++ virtual functions called on a polymorphic base class just as fast as calling a C-style function pointer? Is there really any difference?
Apples and oranges. At a miniscule "one vs. one" kind of level, a virtual function call involves slightly more work as there's an indirection/indexing overhead to get from vptr
to vtable
entry.
But a Virtual Function Call Can Be Faster
Okay, how could this be? I just said a virtual function call requires slightly more work, which is true.
What people tend to forget is to try to make a closer comparison here (to try to make it a little less apples and oranges, even though it is apples and oranges). We typically don't create a class with just one virtual function in it. If we did, then performance (as well as even things like code size) would definitely favor a function pointer. We often have something more like this:
class Foo
{
public:
virtual ~Foo() {}
virtual f1() = 0;
virtual f2() = 0;
virtual f3() = 0;
virtual f4() = 0;
};
... in which case a more "direct" function pointer analogy might be this:
struct Bar
{
void (*f1)();
void (*f2)();
void (*f3)();
void (*f4)();
};
In this kind of case, calling virtual functions in each instance of Foo
can be considerably more efficient than Bar
. It's due to the fact that Foo
only needs to store a single vptr to a central vtable which is being accessed repeatedly. With that we get improved locality of reference (smaller Foos
and ones that can potentially fit better and in number into a cache line, more frequent access of Foo's
central vtable).
Bar
, on the other hand, requires more memory and is effectively duplicating the contents of Foo's
vtable in each instance of Bar
(let's say there are a million instances of Foo
and Bar
). In that case, the amount of redundant data inflating the size of Bar
will often significantly outweigh the cost of doing slightly less work per function pointer call.
If we only need to store one function pointer per object, and this was an extreme hot spot, then it might be good to just store a function pointer (ex: it might be useful for someone implementing anything remotely resembling std::function
to just store a function pointer).
So it's kind of apples and oranges, but if we're modeling a use case anything close to this, the vtable kind of approach which stores a central, shared table of function addresses (in C or C++) can be considerably more efficient.
If we're modeling a use case where we just have one single function pointer stored in an object vs. a vtable which has only one virtual function in it, then the function pointer would be slightly more efficient.
It is UNLIKELY you'll see much of a difference, but like all these things, it's often the small details (such as the compiler needing to pass a this
pointer to a virtual function) that can cause differences in performance. The virtual
function itself is a function pointer "under the hood", so you probably get pretty similar code in both cases, once the compiler has done its thing.
It sounds like a good use of virtual functions, and if someone objected and said "there will be a performance difference", I'd say "prove it". But if you want to avoid having that discussion, make a benchmark (if there isn't one already) that measures the performance of the existing code, refactor it (or some part of it) and compare the results. Ideally, test on a couple of different machines, so that you don't get results that work better on YOUR machine, but not so good on some other types of machines (different generations of processors, different manufacturer or processor, etc).
A virtual function call involves two dereferences, one of them indexed, i.e. something like *(object->_vtable[3])()
.
A call via a function pointer involves one dereference.
A method call also requires passing a hidden argument to be received as this
.
Unless the method body is practically empty and there are no arguments or return values you are most unlikely to notice the difference.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With