Map from the C++ code to the equivalent C code
Map from the C++ code to the equivalent C code
This article compares C and C++ by comparing the C++ code and its equivalent C code. This comparison should give you a better feel of the performance differences between C and C++.
C++ Method Invocations
In the first article in this series we will look at the performance impact of C++ method invocations. This comparison will be carried out by first comparing C++ code and its C equivalent.
C++ Code
// Example class A contains regular and | |
// static member variables and methods. | |
class A | |
{ | |
private: | |
int m_x; | |
static int g_y; | |
int m_z; | |
// Should be invoked when the object ends | |
void InformEnd(); | |
public: | |
A(int x); | |
~A(); | |
void UpdateX(int newX); | |
static void UpdateY(int newY); | |
}; | |
// Initialization of the static variable | |
int A::g_y = 0; | |
// The non-static member variables | |
// are initialized in the constructor | |
A::A(int x) | |
{ | |
m_x = x; | |
m_z = 0; | |
} | |
// Destructor invokes a private variable | |
A::~A() | |
{ | |
InformEnd(); | |
} | |
// UpdateX checks the value of X against | |
// a static variable before updating the value | |
void A::UpdateX(int newX) | |
{ | |
if (g_y != 0 && m_x < newX) | |
{ | |
m_x = newX; | |
} | |
} | |
// Unconditional update of static variable m_y | |
void A::UpdateY(int newY) | |
{ | |
g_y = newY; | |
} | |
main() | |
{ | |
// Create a object on the heap | |
A *pA = new A(5); | |
// Create an object on the stack | |
A a(6); | |
// Example of an access via a pointer | |
pA->UpdateX(8); | |
// Example of a direct access | |
a.UpdateX(9); | |
// Example of static method call | |
A::UpdateY(1000); | |
// Deleting the object | |
delete pA; | |
} |
The following C code provides an equivalent implementation for the C++ code shown above. The C++ class has been mapped to a C structure.
C code
/* | |
This code maps from the C++ code to the equivalent C code. | |
Mapping of the following entities is covered: | |
- classes - methods | |
- this pointer - member variables | |
- constructors - static methods | |
- destructors - static variables | |
*/ | |
#include <stdio.h> | |
#include <stdlib.h> | |
#define TRUE 1 | |
#define FALSE 0 | |
typedef int BOOLEAN; | |
/* | |
Structure A represents the class A. Only the non-static member | |
variables are present in the structure | |
*/ | |
struct A | |
{ | |
int m_x; | |
int m_z; | |
}; | |
/* Notice that g_y is not a part of struct A. Its a separate global variable. */ | |
int g_y = 0; | |
/* | |
Prototype for the InformEnd method. The C++ version of this method | |
did not have any parameters but the C mapped function needs the this | |
pointer to obtain the address of the object. Note that all non-static | |
methods in the C++ code would map to a C function the additional this | |
pointer as the first parameter. | |
*/ | |
void InformEnd(A *this_ptr); | |
/* | |
The constructor maps to function with the this pointer and the size of the | |
structure as parameters. this_ptr passed to the constructor is NULL when | |
the operator new is used to create the object. this_ptr contains a valid | |
pointer if the memory for the object to be constructed is already | |
allocated. (e.g. local variable or part of another structure.) | |
*/ | |
A *A_Constructor(A *this_ptr, int x) | |
{ | |
/*Check if memory has been allocated for struct A. */ | |
if (this_ptr == NULL) | |
{ | |
/*Allocate memory of size A. */ | |
this_ptr = (A *) malloc(sizeof(A)); | |
} | |
/* Once the memory has been allocated for A, initialise members of A. */ | |
if (this_ptr) | |
{ | |
this_ptr->m_x = x; | |
this_ptr->m_z = 0; | |
} | |
return this_ptr; | |
} | |
/* | |
The following function is equivalent to a destructor. The this | |
pointer and a dynamic flag are passed as the two parameters to | |
this function. The dynamic flag is set to true if the object is | |
being deleted using the delete operator. | |
*/ | |
void A_Destructor(A *this_ptr, BOOLEAN dynamic) | |
{ | |
InformEnd(this_ptr); | |
/* If the memory was dynamically allocated for A, explicitly free it. */ | |
if (dynamic) | |
{ | |
free(this_ptr); | |
} | |
} | |
/* | |
A pointer this is passed as first argument. All member variables | |
in the code will be accessed through an indirecion from the this | |
pointer. Notice that static variables are accessed directly as | |
they do not belong to any instance. | |
*/ | |
void A_UpdateX(A *this_ptr, int newX) | |
{ | |
if (g_y != 0 && this_ptr->m_x < newX) | |
{ | |
this_ptr->m_x = newX; | |
} | |
} | |
/* | |
Notice that this is not passed here. This is so because | |
A_UpdateY is a static function. This function can only access | |
other static functions and static or global variables. This | |
function cannot access any member variables or methods of class A | |
as a static function does not correspond to an instance. | |
*/ | |
void A_UpdateY(int newY) | |
{ | |
g_y = newY; | |
} | |
main() | |
{ | |
/* | |
Dynamically allocate memory by passing NULL in this arguement. | |
Also initialize members of struct pointed to by pA. | |
*/ | |
A *pA = A_Constructor(NULL, 5); | |
/* Define local variable a of type struct A. */ | |
A a; | |
/* | |
Initialize members of struct variable a. Note that the | |
constructor is called with the address of the object as | |
a has been pre-allocated on the stack. | |
*/ | |
A_Constructor(&a, 6); | |
/* | |
Method invocations in C++ are handled by calling the | |
corresponding C functions with the object pointer. | |
*/ | |
A_UpdateX(pA, 8); | |
A_UpdateX(&a, 9); | |
/* UpdateY is a static method, so object pointer is not passed */ | |
A_UpdateY(1000); | |
/* | |
Delete memory pointed to by pA (explicit delete in | |
original code). | |
*/ | |
A_Destructor(pA, TRUE); | |
/* | |
Since memory was allocated on the stack for local struct | |
variable a, it will be deallocated when a goes out of scope. | |
The destructor will also be invoked. Notice that dynamic flag | |
is set to false so that the destructor does not try to | |
free memory. | |
*/ | |
A_Destructor(&a, FALSE); | |
} |
Analysis
This section analyses the C++ code and its C translation and identifies the performance impact.
C++ Method Invocation | All C++ methods when translated to C end up with an additional parameter. This might appear to be a big performance overhead. In reality however, the code in C will also have to access the common data structure via an array index or some other mechanism. |
Object Construction | Whenever an object is constructed, C++ will invoke the constructor. Sometimes this might be an addition overhead. This overhead can be reduced by defining the constructor inline. In most cases however, the constructor is actually replacing a routine that would have been used to initialize the data structures in a conventional C program. If a program declares a lot of global objects, object construction can be a big overhead at program startup. C++ invokes constructors for all global objects before main() is called. |
Object Destruction | As you can see from the C code, whenever an object goes out of scope or is explicitly deleted, C++ invokes the destructor for the object. This overhead can be reduced by only defining destructors when they are really needed (i.e. some action is required when object is deleted). Inline destructors can also be used to reduce the overhead. |
Static Access | The C code above shows that static member functions and variables do not correspond to an instance of the object. Thus they are accessed without indirection of the object. This can be useful in defining methods which need C level function call conventions. One good use for static member functions is to implement interrupt service routines (ISRs). ISRs handlers typically need to be C type functions. In most implementations, C++ static functions can be directly used as ISR handlers. |
Virtual Functions and Inheritance
This section presents the C++ code for a typical virtual function invocation scenario. This is then compared to the equivalent C code.
C++ code
// A typical example of inheritance and virtual function use. | |
// We would be mapping this code to equivalent C. | |
// Prototype graphics library function to draw a circle | |
void glib_draw_circle (int x, int y, int radius); | |
// Shape base class declaration | |
class Shape | |
{ | |
protected: | |
int m_x; // X coordinate | |
int m_y; // Y coordinate | |
public: | |
// Pure virtual function for drawing | |
virtual void Draw() = 0; | |
// A regular virtual function | |
virtual void MoveTo(int newX, int newY); | |
// Regular method, not overridable. | |
void Erase(); | |
// Constructor for Shape | |
Shape(int x, int y); | |
// Virtual destructor for Shape | |
virtual ~Shape(); | |
}; | |
// Circle class declaration | |
class Circle : public Shape | |
{ | |
private: | |
int m_radius; // Radius of the circle | |
public: | |
// Override to draw a circle | |
virtual void Draw(); | |
// Constructor for Circle | |
Circle(int x, int y, int radius); | |
// Destructor for Circle | |
virtual ~Circle(); | |
}; | |
// Shape constructor implementation | |
Shape::Shape(int x, int y) | |
{ | |
m_x = x; | |
m_y = y; | |
} | |
// Shape destructor implementation | |
Shape::~Shape() | |
{ | |
//... | |
} | |
// Circle constructor implementation | |
Circle::Circle(int x, int y, int radius) : Shape (x, y) | |
{ | |
m_radius = radius; | |
} | |
// Circle destructor implementation | |
Circle::~Circle() | |
{ | |
//... | |
} | |
// Circle override of the pure virtual Draw method. | |
void Circle::Draw() | |
{ | |
glib_draw_circle(m_x, m_y, m_radius); | |
} | |
main() | |
{ | |
// Define a circle with a center at (50,100) and a radius of 25 | |
Shape *pShape = new Circle(50, 100, 25); | |
// Define a circle with a center at (5,5) and a radius of 2 | |
Circle aCircle(5,5, 2); | |
// Various operations on a Circle via a Shape pointer | |
pShape->Draw(); | |
pShape->MoveTo(100, 100); | |
pShape->Erase(); | |
delete pShape; | |
// Invoking the Draw method directly | |
aCircle.Draw(); | |
} |
C code
/* | |
The following code maps the C++ code for the Shape and Circle classes | |
to C code. | |
*/ | |
#include <stdio.h> | |
#include <stdlib.h> | |
#define TRUE 1 | |
#define FALSE 0 | |
typedef int BOOLEAN; | |
/* | |
Error handler used to stuff dummy VTable | |
entries. This is covered later. | |
*/ | |
void pure_virtual_called_error_handler(); | |
/* Prototype graphics library function to draw a circle */ | |
void glib_draw_circle (int x, int y, int radius); | |
typedef void (*VirtualFunctionPointer)(...); | |
/* | |
VTable structure used by the compiler to keep | |
track of the virtual functions associated with a class. | |
There is one instance of a VTable for every class | |
containing virtual functions. All instances of | |
a given class point to the same VTable. | |
*/ | |
struct VTable | |
{ | |
/* | |
d and i fields are used when multiple inheritance and virtual | |
base classes are involved. We will be ignoring them for this | |
discussion. | |
*/ | |
int d; | |
int i; | |
/* | |
A function pointer to the virtual function to be called is | |
stored here. | |
*/ | |
VirtualFunctionPointer pFunc; | |
}; | |
/* | |
The Shape class maps into the Shape structure in C. All | |
the member variables present in the class are included | |
as structure elements. Since Shape contains a virtual | |
function, a pointer to the VTable has also been added. | |
*/ | |
struct Shape | |
{ | |
int m_x; | |
int m_y; | |
/* | |
The C++ compiler inserts an extra pointer to a vtable which | |
will keep a function pointer to the virtual function that | |
should be called. | |
*/ | |
VTable *pVTable; | |
}; | |
/* | |
Function prototypes that correspond to the C++ methods | |
for the Shape class, | |
*/ | |
Shape *Shape_Constructor(Shape *this_ptr, int x, int y); | |
void Shape_Destructor(Shape *this_ptr, bool dynamic); | |
void Shape_MoveTo(Shape *this_ptr, int newX, int newY); | |
void Shape_Erase(Shape *this_ptr); | |
/* | |
The Shape vtable array contains entries for Draw and MoveTo | |
virtual functions. Notice that there is no entry for Erase, | |
as it is not virtual. Also, the first two fields for every | |
vtable entry are zero, these fields might have non zero | |
values with multiple inheritance, virtual base classes | |
A third entry has also been defined for the virtual destructor | |
*/ | |
VTable VTableArrayForShape[] = | |
{ | |
/* | |
Vtable entry virtual function Draw. | |
Since Draw is pure virtual, this entry | |
should never be invoked, so call error handler | |
*/ | |
{ 0, 0, (VirtualFunctionPointer) pure_virtual_called_error_handler }, | |
/* | |
This vtable entry invokes the base class's | |
MoveTo method. | |
*/ | |
{ 0, 0, (VirtualFunctionPointer) Shape_MoveTo }, | |
/* Entry for the virtual destructor */ | |
{ 0, 0, (VirtualFunctionPointer) Shape_Destructor } | |
}; | |
/* | |
The struct Circle maps to the Circle class in the C++ code. | |
The layout of the structure is: | |
- Member variables inherited from the the base class Shape. | |
- Vtable pointer for the class. | |
- Member variables added by the inheriting class Circle. | |
*/ | |
struct Circle | |
{ | |
/* Fields inherited from Shape */ | |
int m_x; | |
int m_y; | |
VTable *pVTable; | |
/* Fields added by Circle */ | |
int m_radius; | |
}; | |
/* | |
Function prototypes for methods in the Circle class. | |
*/ | |
Circle *Circle_Constructor(Circle *this_ptr, int x, int y, int radius); | |
void Circle_Draw(Circle *this_ptr); | |
void Circle_Destructor(Circle *this_ptr, BOOLEAN dynamic); | |
/* Vtable array for Circle */ | |
VTable VTableArrayForCircle[] = | |
{ | |
/* | |
Vtable entry virtual function Draw. | |
Circle_Draw method will be invoked when Shape's | |
Draw method is invoked | |
*/ | |
{ 0, 0, (VirtualFunctionPointer) Circle_Draw }, | |
/* | |
This vtable entry invokes the base class's | |
MoveTo method. | |
*/ | |
{ 0, 0, (VirtualFunctionPointer) Shape_MoveTo }, | |
/* Entry for the virtual destructor */ | |
{ 0, 0, (VirtualFunctionPointer) Circle_Destructor } | |
}; | |
Shape *Shape_Constructor(Shape *this_ptr, int x, int y) | |
{ | |
/* Check if memory has been allocated for struct Shape. */ | |
if (this_ptr == NULL) | |
{ | |
/* Allocate memory of size Shape. */ | |
this_ptr = (Shape *) malloc(sizeof(Shape)); | |
} | |
/* | |
Once the memory has been allocated for Shape, | |
initialise members of Shape. | |
*/ | |
if (this_ptr) | |
{ | |
/* Initialize the VTable pointer to point to shape */ | |
this_ptr->pVTable = VTableArrayForShape; | |
this_ptr->m_x = x; | |
this_ptr->m_y = y; | |
} | |
return this_ptr; | |
} | |
void Shape_Destructor(Shape *this_ptr, BOOLEAN dynamic) | |
{ | |
/* | |
Restore the VTable to that for Shape. This is | |
required so that the destructor does not invoke | |
a virtual function defined by a inheriting class. | |
(The base class destructor is invoked after inheriting | |
class actions have been completed. Thus it is not | |
safe to invoke the ineriting class methods from the | |
base class destructor) | |
*/ | |
this_ptr->pVTable = VTableArrayForShape; | |
/*...*/ | |
/* | |
If the memory was dynamically allocated | |
for Shape, explicitly free it. | |
*/ | |
if (dynamic) | |
{ | |
free(this_ptr); | |
} | |
} | |
Circle *Circle_Constructor(Circle *this_ptr, int x, int y, int radius) | |
{ | |
/* Check if memory has been allocated for struct Circle. */ | |
if (this_ptr == NULL) | |
{ | |
/* Allocate memory of size Circle. */ | |
this_ptr = (Circle *) malloc(sizeof(Circle)); | |
} | |
/* | |
Once the memory has been allocated for Circle, | |
initialise members of Circle. | |
*/ | |
if (this_ptr) | |
{ | |
/* Invoking the base class constructor */ | |
Shape_Constructor((Shape *)this_ptr, x, y); | |
this_ptr->pVTable = VTableArrayForCircle; | |
this_ptr->m_radius = radius; | |
} | |
return this_ptr; | |
} | |
void Circle_Destructor(Circle *this_ptr, BOOLEAN dynamic) | |
{ | |
/* Restore the VTable to that for Circle */ | |
this_ptr->pVTable = VTableArrayForCircle; | |
/*...*/ | |
/* | |
Invoke the base class destructor after ineriting class | |
destructor actions have been completed. Also note that | |
that the dynamic flag is set to false so that the shape | |
destructor does not free any memory. | |
*/ | |
Shape_Destructor((Shape *) this_ptr, FALSE); | |
/* | |
If the memory was dynamically allocated | |
for Circle, explicitly free it. | |
*/ | |
if (dynamic) | |
{ | |
free(this_ptr); | |
} | |
} | |
void Circle_Draw(Circle *this_ptr) | |
{ | |
glib_draw_circle(this_ptr->m_x, this_ptr->m_y, this_ptr->m_radius); | |
} | |
main() | |
{ | |
/* | |
Dynamically allocate memory by passing NULL in this arguement. | |
Also initialse members of struct pointed to by pShape. | |
*/ | |
Shape *pShape = (Shape *) Circle_Constructor(NULL, 50, 100, 25); | |
/* Define a local variable aCircle of type struct Circle. */ | |
Circle aCircle; | |
/* Initialise members of struct variable aCircle. */ | |
Circle_Constructor(&aCircle, 5, 5, 2); | |
/* | |
Virtual function Draw is called for the shape pointer. The compiler | |
has allocated 0 offset array entry to the Draw virtual function. | |
This code corresponds to "pShape->Draw();" | |
*/ | |
(pShape->pVTable[0].pFunc)(pShape); | |
/* | |
Virtual function MoveTo is called for the shape pointer. The compiler | |
has allocared 1 offset array entry to the MoveTo virtual function. | |
This code corresponds to "pShape->MoveTo(100, 100);" | |
*/ | |
(pShape->pVTable[1].pFunc)(pShape, 100, 100); | |
/* | |
The following code represents the Erase method. This method is | |
not virtual and it is only defined in the base class. Thus | |
the Shape_Erase C function is called. | |
*/ | |
Shape_Erase(pShape); | |
/* Delete memory pointed to by pShape (explicit delete in original code). | |
Since the destructor is declared virtual, the compiler has allocated | |
2 offset entry to the virtual destructor | |
This code corresponds to "delete pShape;". | |
*/ | |
(pShape->pVTable[2].pFunc)(pShape, TRUE); | |
/* | |
The following code corresponds to aCircle.Draw(). | |
Here the compiler can invoke the method directly instead of | |
going through the vtable, since the type of aCircle is fully | |
known. (This is very much compiler dependent. Dumb compilers will | |
still invoke the method through the vtable). | |
*/ | |
Circle_Draw(&aCircle); | |
/* | |
Since memory was allocated from the stack for local struct | |
variable aCircle, it will be deallocated when aCircle goes out of scope. | |
The destructor will also be invoked. Notice that dynamic flag is set to | |
false so that the destructor does not try to free memory. Again, the | |
compiler does not need to go through the vtable to invoke the destructor. | |
*/ | |
Circle_Destructor(&aCircle, FALSE); | |
} |
C code implementing the above C++ functionality is shown below. The code also includes compiler generated constructs like vtables. Virtual function access using vtables is also covered. (The presentation here has been simplified here to aid understanding).
Analysis
This section analyses the C++ code and its C translation and identifies the performance impact.
Object Construction | Inheritance does increase the object construction overhead, as constructors for all the parent classes in the class hierarchy are invoked. There is the additional overhead of setting up vtable in the constructor. |
Object Destruction | Inheritance does increase the object destruction overhead, as destructors for all the parent classes in the class hierarchy are invoked. There is the additional overhead of setting up vtable in the destructor. Virtual destructors also increase the overhead of object destruction. |
Virtual Function Invocation | Virtual function invocation is slightly more expensive than invoking a function through a function pointer. In many scenarios, intelligent compilers can use normal method invocation instead of a virtual function invocation. In a well designed object oriented system, a virtual function call would typically replace a switch statement so virtual function invocation might actually be faster than conventional coding techniques. For example, a generic draw statement in a "C" based paint program would involve switching over the type of shape and then invoking the corresponding draw function. In C++, this logic will be replaced by a virtual function call. In our experience we have found that poorly designed and excessive use of constructors and destructors reduces performance much more than virtual function calls. |
Memory Overhead | Using plain inheritance has no memory overhead. Inheritance
with virtual functions however does introduce the following memory
overhead:
|
Locality of Reference | In the current computing environment, processor speeds have
increased considerably but memory access speeds haven't kept pace. In such
a scenario, cache hit ratio of an application plays a very important role
in determining application performance.
In general this turns out to be an advantage for programs written in C++. With C++ code and data locality of reference is much better than C, as all the class code manipulating object data is located together. Also all object data is located at one place. In C code and data are scattered all over the place. Thus a C++ program should offer a better locality of reference than a C program. In many cases this might more than compensate for the performance overhead of C++. |
Multiple Inheritance and Virtual Base Classes | In this article we have not covered multiple inheritance and virtual base classes. There is a significant increase in overhead due to these features. We would recommend that you stay away from using these features. |