Thursday, February 21, 2008

Virtual Overhead

When designing a library, declaring virtual methods in a base class comes from time to time. But do we exactly know what is the overhead associated with that decision.

There are various overheads:
  • Size : The instances of such classes take more space
  • Time : Calling virtual methods takes time
I am going to measure both. First I am going to take a look at the size overhead.

Consider the code below:

using namespace std;

class
VirtualObject
{

public
:
virtual
int foo (int i, int j) = 0;
virtual
~VirtualObject()
{

}
};


class
DerivedVirtual : public VirtualObject
{

private
:
int
x;
public
:
int
foo (int i, int j) {
++
j;
return
j;
}
};


class
ConcreteObject
{

public
:
int
foo (int i, int j)
{

return
j;
}
};


class
DerivedConcrete : public ConcreteObject
{

private
:
int
x;
public
:
int
foo (int i, int j) {
++
j;
return
j;
}
};


void
test_virtual_object_size ()
{

cout << "sizeof(VirtualObject): " ;
cout << sizeof(VirtualObject) << endl;
cout << "sizeof(DerivedVirtual): " ;
cout << sizeof(DerivedVirtual) << endl;
cout << "sizeof(ConcreteObject): " ;
cout << sizeof(ConcreteObject) << endl;
cout << "sizeof(DerivedConcrete): " ;
cout << sizeof(DerivedConcrete) << endl;
}


#define TEST_INNER_LOOP 1000000
#define TEST_OUTER_LOOP 100

void
test_virtual_call_overhead ()
{

Core::Timer timer;
long long
x;

x = 0;
DerivedVirtual dv;
VirtualObject & vo = dv;
timer.start();
for
( int i=0; i < TEST_OUTER_LOOP; ++i )
{

for
( int j=0; j < TEST_INNER_LOOP; ++j )
{

x += vo.foo(i,j);
}
}

cout << "DerivedVirtual::foo() " << x << " " << timer.elapsedTimeInSeconds() << endl;


x = 0;
DerivedConcrete dc;
timer.start();
for
( int i=0; i < TEST_OUTER_LOOP; ++i )
{

for
( int j=0; j < TEST_INNER_LOOP; ++j )
{

x += dc.foo(j,i);
}
}

cout << "DerivedConcrete::foo() " << x << " " << timer.elapsedTimeInSeconds() << endl;
}




In my system I get the following output for test_virtual_object_size

sizeof(VirtualObject): 4
sizeof(DerivedVirtual): 8
sizeof(ConcreteObject): 1
sizeof(DerivedConcrete): 4

This essentially means 4 byte for each object. It can be a significant overhead especially for small size objects.

For virtual call overhead this is what I get on release mode:
DerivedVirtual::foo() 50000050000000 0.423278
DerivedConcrete::foo() 5050000000 1e-06

But on debug mode this is what I get:
DerivedVirtual::foo() 50000050000000 0.832059
DerivedConcrete::foo() 5050000000 0.710893

The difference is mostly because of inlining. The compiler can not inline virtual functions, thus the overhead becomes significant in release mode.

One last note is that you incur the virtual call overhead only if you call the virtual function through a pointer or reference. This is important. If we used dv, instead of vo within the loop we would have identical results as the non-virtual function call. However, notice that dv is a DerivedVirtual object, not a pointer. If you write a function like the following, you will again incur the virtual call overhead.

void test1 ( DerivedVirtual & pdv )
{

Core::Timer timer;
long long
x;

x = 0;
timer.start();
for
( int i=0; i < TEST_OUTER_LOOP; ++i )
{

for
( int j=0; j < TEST_INNER_LOOP; ++j )
{

x += pdv.foo(i,j);
}
}

cout << "DerivedVirtual::foo() " << x << " " << timer.elapsedTimeInMilliSeconds() << endl;


}
In this case, we would incur the virtual function overhead. Instead of using a reference to DerivedVirtual, if we had used a DerivedVirtual object we would not have the virtual call overhead.

If C++ had a facility to declare a class as final or sealed, the compiler could have used that information to optimize these virtual calls, since pdv.foo had to be the DerivedVirtual's foo and nothing else. Even though there is no other class deriving from DerivedVirtual, we still see the virtual call overhead.

No comments: