Home arrow C++ arrow Page 3 - C++ in theory: Bridging Your Classes with PIMPLs
C++

C++ in theory: Bridging Your Classes with PIMPLs


Very often, when a program takes longer to compile after you have made what appear to be trivial changes, the blame can be laid at the door of dependency chains between header files. One change can trigger the need for a massive rebuild. J. Nakamura explains a way to make header files insensitive to any change -- thus saving all that rebuild time -- by using pimpl.

Author Info:
By: J. Nakamura
Rating: 4 stars4 stars4 stars4 stars4 stars / 10
February 01, 2005
TABLE OF CONTENTS:
  1. · C++ in theory: Bridging Your Classes with PIMPLs
  2. · The Private Implementation
  3. · Pimpl Drawbacks
  4. · Summary

print this article
SEARCH DEVARTICLES

C++ in theory: Bridging Your Classes with PIMPLs - Pimpl Drawbacks
(Page 3 of 4 )

You do pay a price for pimpls when it comes to performance. Let's set up some code:

#include <stdio.h>
#include <time.h>

class MyClassA {
public:
  MyClassA( ) : c(0) {}
  char foo( ) { return c; }
  char c;
};

class MyClassB {
public:
  MyClassB( ) : m_pImpl(new MyClassA) { }
  ~MyClass( ) { try { delete m_pImpl; } catch ( ... ) { } }
  char foo( ) { return m_pImpl->foo( ); }
private:
  char c;
  MyClassA *m_pImpl;
};


Here we have set up MyClassA to be the private implementation of MyClassB. We are going to measure how much performance overhead the function indirection and memory allocation require.

int main(int argc, char *argv[])
{
clock_t start, finish; // stores time values
for (int count = 0; count < 10; ++count)
{
  /* our test code will go here */
}
return 0;
}

Each construction/deconstruction needs to allocate/free memory.

Since the implementation is hidden in a separate class behind a pointer, every time our pimpled class is created, we need to allocate memory on the heap. And every time our pimpled class is destroyed, we need to free memory. Compared to common operations like function calls, memory allocation and deallocation are relative expensive operations.

Normally you would not notice or worry about this performance cost, but when you have to construct/destruct an array of your pimpled object repeatedly, you will notice a difference:

  start = clock();
  for (int i = 0; i < 10; ++i )  MyClassA arryA[0xffff];
  finish = clock();
  int durationA = finish – start;
  (void)printf(“ticks spent on MyClassA: %d\n”, durationA);

  start = clock();
  for (int j = 0; j < 10; ++j )  MyClassB arryB[0xffff];
  finish = clock();
  int durationB = finish – start;
  (void)printf(“ticks spent on MyClassB: %d\n”, durationB);

clock() calculates the processor time used by the calling process; we can use this to measure how much time is spent on constructing and destructing MyClassA arryA[0xffff] and MyClassB arryB[0xffff] 10 times. I prefer to cast the result of printf to void (it returns the number of characters printed or a negative value if an error occurs), just to make it clear that we are ignoring the return value.

When you run the sample, you will notice that the construction of the pimpled class takes a lot more time than the non-pimpled one. On my PIV 3Ghz the result was:

ticks spent on MyClassA: 31
ticks spent on MyClassB: 719

Though the sample might be a bit extreme, you can see that the difference is not trivial. Remember that the construction of an n-array of objects, constructs n objects on the stack for you. In this case it means that the MyClassB constructor is called n times, performing n allocations on the heap!

To make things worse, this example doesn’t do anything with the array, and since it immediately goes out of scope, the destructor is called n times as well.

Access to hidden members comes at the cost of at least one extra indirection.

Each access of a member in the pimpled class can require at least one extra indirection. A pointer dereference can be quite an extra cost if the code is called often:

  MyClassA aObj;
  start = clock();
  for (int k = 0; k < 0xffffff; ++k)  (void)aObj.foo();
  finish = clock();
  durationA = finish – start;
  (void)printf(“ticks spent calling MyClassA::foo(): %d\n”, durationA);

  MyClassB bObj;
  start = clock();
  for (int l = 0; l < 0xffffff; ++l)  (void) bObj.foo();
  finish = clock();
  durationB = finish – start;
  (void)printf(“ticks spent calling MyClassB::foo(): %d\n”, durationB);

Running another extreme sample, my result was:

ticks spent calling MyClassA::foo(): 1500
ticks spent calling MyClassB::foo(): 3156

Again, this is not a trivial difference when the code is executed often!

Access to public members comes at the cost of an extra indirection.

Sometimes the private implementation needs to access public member functions of the pimpled class. Storing a “back pointer” to the visible object can easily do this. By convention, this back pointer is usually named “self.”

// MyClass.h
class MyClass {
public:
  MyClass();
  ~MyClass();
  void foo();
void bar();
private:
  MyClassA *m_pImpl;
};

// MyClass.cpp
class MyClassImpl {
public:
  MyClassImpl(MyClass *_self) : self(_self) {}
  void foo() { self->bar(); }
};
MyClass::MyClass()
  : m_pImpl(new MyClassImpl(this))
{}
MyClass::~MyClass()
{
try { delete m_pImpl; }
catch (...) {}
}

Looking at the previous example, it is clear that an extra indirection back will drive the performance cost even further up.

There is a small space overhead.

“Space overhead?” you might ask -- and you might not think this is a big deal. When you have a lot of small objects which you have pimpled, however, that extra pointer does start to count (when you need a back pointer, you will actually need two extra pointers). Try the following on the test code above:

  (void)printf(“sizeof MyClassA: %d\n”, sizeof(MyClassA));
  (void)printf(“sizeof MyClassB: %d\n”, sizeof(MyClassB));

Result will vary among compilers but the overhead I have is actually 7 bytes:

sizeof MyClassA: 1
sizeof MyClassB: 8

Even though a pointer only costs 4 bytes on my machine, the compiler aligns it on a 4-byte boundary, wasting 3 bytes (char c takes one byte). The overhead will be even larger on an AS/400 or 64-bit machine. Of course you can configure this in your build settings...but that is not my point.


blog comments powered by Disqus
C++ ARTICLES

- Intel Threading Building Blocks
- Threading Building Blocks with C++
- Video Memory Programming in Text Mode
- More Tricks to Gain Speed in Programming Con...
- Easy and Efficient Programming for Contests
- Preparing For Programming Contests
- Programming Contests: Why Bother?
- Polymorphism in C++
- Overview of Virtual Functions
- Inheritance in C++
- Extending the Basic Streams in C++
- Using Stringstreams in C++
- Custom Stream Manipulation in C++
- General Stream Manipulation in C++
- Serialize Your Class into Streams in C++

Watch our Tech Videos 
Dev Articles Forums 
 RSS  Articles
 RSS  Forums
 RSS  All Feeds
Write For Us 
Weekly Newsletter
 
Developer Updates  
Free Website Content 
Contact Us 
Site Map 
Privacy Policy 
Support 

Developer Shed Affiliates

 




© 2003-2017 by Developer Shed. All rights reserved. DS Cluster - Follow our Sitemap
Popular Web Development Topics
All Web Development Tutorials