12
votes

I wrote a simple example, which estimates average time of calling virtual function, using base class interface and dynamic_cast and call of non-virtual function. Here is it:

#include <iostream>
#include <numeric>
#include <list>
#include <time.h>

#define CALL_COUNTER (3000)

__forceinline int someFunction()
{
  return 5;
}

struct Base
{
  virtual int virtualCall() = 0;
  virtual ~Base(){};
};

struct Derived : public Base
{
  Derived(){};
  virtual ~Derived(){};
  virtual int virtualCall(){ return someFunction(); };
  int notVirtualCall(){ return someFunction(); };
};


struct Derived2 : public Base
{
  Derived2(){};
  virtual ~Derived2(){};
  virtual int virtualCall(){ return someFunction(); };
  int notVirtualCall(){ return someFunction(); };
};

typedef std::list<double> Timings;

Base* createObject(int i)
{
  if(i % 2 > 0)
    return new Derived(); 
  else 
    return new Derived2(); 
}

void callDynamiccast(Timings& stat)
{
  for(unsigned i = 0; i < CALL_COUNTER; ++i)
  {
    Base* ptr = createObject(i);

    clock_t startTime = clock();

    for(int j = 0; j < CALL_COUNTER; ++j)
    {
      Derived* x = (dynamic_cast<Derived*>(ptr));
      if(x) x->notVirtualCall();
    }

    clock_t endTime = clock();
    double callTime = (double)(endTime - startTime) / CLOCKS_PER_SEC;
    stat.push_back(callTime);

    delete ptr;
  }
}

void callVirtual(Timings& stat)
{
  for(unsigned i = 0; i < CALL_COUNTER; ++i)
  {
    Base* ptr = createObject(i);

    clock_t startTime = clock();

    for(int j = 0; j < CALL_COUNTER; ++j)
      ptr->virtualCall();


    clock_t endTime = clock();
    double callTime = (double)(endTime - startTime) / CLOCKS_PER_SEC;
    stat.push_back(callTime);

     delete ptr;
  }
}

int main()
{
  double averageTime = 0;
  Timings timings;


  timings.clear();
  callDynamiccast(timings);
  averageTime = (double) std::accumulate<Timings::iterator, double>(timings.begin(), timings.end(), 0);
  averageTime /= timings.size();
  std::cout << "time for callDynamiccast: " << averageTime << std::endl;

  timings.clear();
  callVirtual(timings);
  averageTime = (double) std::accumulate<Timings::iterator, double>(timings.begin(), timings.end(), 0);
  averageTime /= timings.size();
  std::cout << "time for callVirtual: " << averageTime << std::endl;

  return 0;
}

It looks like callDynamiccast takes almost two times more.

time for callDynamiccast: 0.000240333

time for callVirtual: 0.0001401

Any ideas why does it?

EDITED: object creation is made in separete function now, so the compler does not know it real type. Almost the same result.

EDITED2: create two different types of a derived objects.

4
You probably need to run a lot more iterations that that to get a decent statistical measure. Are you compiling at the highest optimisation setting?Oliver Charlesworth
Your test is invalid, because the compiler can easily optimize both the virtual call (into a nonvirtual call) and the dynamic_cast (into basically a noop), because it knows that ptr really points to a Derived object.Johannes Schaub - litb
Write a function Base* createBase() which randomly returns Base* or Derived* and call it in each loop iteration.ipc
Ehm, the title says dynamic_cast is faster but the numbers show virtual is faster. Which is expected, I think.user180326
@DmitryEskin: If you've turned optimisations off, then the results of the test mean absolutely nothing...Oliver Charlesworth

4 Answers

19
votes

The virtual function call is similar to a function pointer, or if the compiler knows the type, static dispatch. This is constant time.

dynamic_cast is quite different -- it uses an implementation defined means to determine a type. It is not constant time, may traverse the class hierarchy (also consider multiple inheritance) and perform several lookups. An implementation may use string comparisons. Therefore, the complexity is higher in two dimensions. Real time systems often avoid/discourage dynamic_cast for these reasons.

More details are available in this document.

10
votes

It should be noted that the entire purpose of virtual functions is to not have to cast down the inheritance graph. Virtual functions exist so that you can use a derived class instance as though it were a base class. So that more specialized implementations of functions can be called from code that originally called base class versions.

If virtual functions were slower than a safe cast to the derived-class + function call, then C++ compilers would simply implement virtual function calls that way.

So there's no reason to expect dynamic_cast+call to be faster.

5
votes

You are just measuring the cost of dynamic_cast<>. It is implemented with RTTI, that's optional in any C++ compiler. Project + Properties, C/C++, Language, Enable Run-Time Type Info setting. Change it to No.

You'll now get an unsubtle reminder that dynamic_cast<> can no longer do the proper job. Arbitrarily change it to static_cast<> to get drastically different results. Key point here is that if you know that an upcast is always safe then static_cast<> buys you the performance you are looking for. If you don't know for a fact that the upcast is safe then dynamic_cast<> keeps you out of trouble. It is the kind of trouble that is maddingly hard to diagnose. The common failure mode is heap corruption, you only get an immediate GPF if you are really lucky.

2
votes

The difference is, that you can call the virtual function on any instance that is derived from Base. The notVirtualCall() member does not exist within Base, and cannot be called without first determining the exact dynamic type of the object.

The consequence of this difference is, that the vtable of the base class includes a slot for virtualCall(), which contains a function pointer to the correct function to call. So, the virtual call simply chases the vtable pointer included as the first (invisible) member of all objects of type Base, loads the pointer from the slot corresponding to virtualCall(), and calls the function behind that pointer.

When you do a dynamic_cast<>, by contrast, the class Base does not know at compile time what other classes will eventually derive from it. Consequently, it cannot include information within its vtable that eases the resolution of the dynamic_cast<>. That is the information lack that makes a dynamic_cast<> more expensive to implement than a virtual function call. The dynamic_cast<> has to actually search through the inheritance tree of the actual object to check whether the destination type of the cast is found among its bases. That is work that the virtual call avoids.