View Single Post
Old 04-12-2005, 09:12 AM   #1 (permalink)
Valmont
[code][/code] enforcer
 
Valmont's Avatar
 
Join Date: Mar 2003
Location: Netherlands
Posts: 1,544
Valmont is on a distinguished road
TIP #3: PRE/POST increment analisys.

Intro

Each week I try to post a new tip, and make it sticky for a week or so. This time it's a fun one: testing the pre- and post increment operator. And this time I can't promise a 5 minute read as well. But it is worth it. And like I mentioned, it is absolutely fun to play with this.

Introducing the pre- and post increment operator

One of the first things we learn as C++ students is the cool pre/post increment operator:
Code:
int a =5;
cout<<a++<<endl; //Post increment.
cout<<a<<endl;
cout<<++a<<endl; //Pre increment.
This works for doubles as well. Only the integer part will be increased. Anyway, at one point we are motivated to use this feature in loops:
Code:
for(unsigned i = 0; i < 35; ++i)
{ //code here }
So nothing new until now. Just making sure our noses point in the same direction.

Urban Legends (?)

Then at one point we hear stories about performance.
'Pre-increment is faster then post-increment.'
Or for the more cautious: pre-increment might be faster. We have to figure this one out carefully. We will. This thread is all about that.

There is another story.
'The compiler will optimize post-increment if you don't use its return value.'
This is what they are talking about:
Code:
cout<<a++<<endl; //Return value used.
a++; //Return value not used.
cout<<a<<endl; //Not relevant.
On the 2nd line, the return value is not used, thus the compile will optimize this line by rewriting it as a pre-increment operator. Well, that's the story anyway. Is this really true?
Yes and no. Or, not persé.
The compiler only knows about the built in types. Actually the compiler is only allowed to know about standard types. Int's, double's, complex are examples of standard types. This is relevant because the compiler should know the semantics of the type if it wants to optimize it away. Your custom Student class, custom String class or custom Complex class maybe usefull, but its semantics are not known by the compiler. So for them the compiler can't optimize this operator. Not really. You could try to provide a hint with they keword inline but that doesn't guarantee anything. Some even don't like it that way. I personally don't know.

But for built-in types, the compiler may optimize the post-increment indeed, if the return value isn't used. But then again, every experianced programmer needed to program on various types of compilers. And they know they shouldn't take anything for granted from compilers. So here is our first motivation to always use the pre-increment operator when possible! Saves us trouble.

Why could post-increment be slower?

Aha, finally the smoking part. They say the post-increment might be slower because the post-increment operator may create a temporary object. And that temporary object may call a (copy) constructor, which in turn demand some cpu time. It is definatly time to demonstrate how a post- and pre-increment implementation looks like.

First the pre-increment operator:
Code:
T& T::operator++()
{
   ++*this;
   return *this;   
}
And now the post-increment operator:
Code:
const T T::operator++(int)
{
   T old(*this);
   ++*this;
   return old;
}
As you can see, the post-increment version needs to create a temporary object ("old"). And this temporary object calls its copy constructor. The copy constructor will ask for some cpu time as well, and that's where we lose some performance. typically in loops this performance hit will be noticable.

Time To Prove Things.

Yeah OK, but then I'll need to write some code. But first I'll tell you my global plan.
  • First, I'll fetch my stopwatch implementation. It is a Win32 application, based on rdtsc for Intel cpu's.
    If you don't know what that is then don't worry, it is not relevant actually.
  • Then I'll write a class that implements both operator++() and operator++(int).
  • It will hold a copy constructor as well. For our test class that is not really needed design wise, but I need it for demonstrational purposes. Duh.
  • Our test class is just a fantasy class: it does nothing usefull at all. It is only a wrapper for a double variable.
  • We'll also implement a ostream operator to ease up on output to screen. For the fun of it, observe how operator<< is not a friend of our test class.
    And if you don't know what I am talking about again, then just ignore: not relevant at all basically.
  • Then I'll setup a small test showing our test class works.
  • Then I'll create a stopwatch object.
  • Then we test pre-increment first.
  • Reset the stopwatch.
  • Test post-increment.

The Class Declaration
Code:
class T
{
public:
  explicit T(double ir = 0.0);
  T(const T& itsObj);
public:
  double get_ir() const;
public:
  inline T& operator++();
  inline const T operator++(int);
private:
  double InternalRep_;
};
The Class Definition
Code:
T::T(double ir) : InternalRep_(ir)
{}

//---------------------------------------

T::T(const T& itsObj)
{ 
  InternalRep_ = itsObj.InternalRep_;
}

//---------------------------------------

double T::get_ir() const
{
  return InternalRep_;
}

//---------------------------------------

inline T& T::operator++()
{
  ++InternalRep_;
  return *this;
}

//---------------------------------------

inline const T T::operator++(int)
{
  T current(*this);
  ++InternalRep_;
  return current;
}

//---------------------------------------

std::ostream& operator<<(std::ostream& os, const T& itsT)
{
  return os<<itsT.get_ir();
}
The main()
Code:
int main()
{
  //The following 5 lines prove (more or less) that this all works nicely.
  T myT(5.39999);
  cout << myT << endl;
  cout<< ++myT <<endl;
  cout << myT++ <<endl;
  cout << myT <<endl;
  
  //And below we are setting up a performance test. 
  //First let's create stopwatch object. By default it is in 1/10th micro secs!
  KWStopWatch sw;
  //I'll create a new object for PRE test.
  T PRE(12.01);
  
  //Test PRE-increment performance.
  sw.start();
  for(unsigned i = 0; i < 50000000; ++i)
  {
    ++PRE;
  }
  sw.stop();
  cout<<endl << "Final time PRE-INCR (ms): " << sw.get_final_time()/10000 <<endl;
  
  //Reset the stopwatch and create a new object for POST test.
  sw.reset();
  T POST(12.01);

  //Test POST-increment performance.
  sw.start();
  for(unsigned i = 0; i < 50000000; ++i)
  {
    POST++;
  }
  sw.stop();
  cout<<endl << "Final time POST-INCR (ms): " << sw.get_final_time()/10000 <<endl;  
  
  //Freeze frame (optional).
  std::cin.get();
  return 0;
}
This is the big picture. But I was being lazy and so I've implented everything in 1 cpp file. It looks like this:
Code:
//We are going to test for performance. Acc = 1/10 micro-s!
#include "hpwin32stopwatch.h" 
#include <iostream>

//--
using std::endl;
using std::cout;
//--

class T
{
public:
  explicit T(double ir = 0.0);
  T(const T& itsObj);
public:
  double get_ir() const;
public:
  inline T& operator++();
  inline const T operator++(int);
private:
  double InternalRep_;
};

//---------------------------------------

T::T(double ir) : InternalRep_(ir)
{}

//---------------------------------------

T::T(const T& itsObj)
{ 
  InternalRep_ = itsObj.InternalRep_;
}

//---------------------------------------

double T::get_ir() const
{
  return InternalRep_;
}

//---------------------------------------

inline T& T::operator++()
{
  ++InternalRep_;
  return *this;
}

//---------------------------------------

inline const T T::operator++(int)
{
  T current(*this);
  ++InternalRep_;
  return current;
}

//---------------------------------------

std::ostream& operator<<(std::ostream& os, const T& itsT)
{
  return os<<itsT.get_ir();
}  

//---------------------------------------

int main()
{
  //The following 5 lines prove (more or less) that this all works nicely.
  T myT(5.39999);
  cout << myT << endl;
  cout<< ++myT <<endl;
  cout << myT++ <<endl;
  cout << myT <<endl;
  
  //And below we are setting up a performance test. 
  //First let's create stopwatch object. By default it is in 1/10th micro secs!
  KWStopWatch sw;
  //I'll create a new object for PRE test.
  T PRE(12.01);
  
  //Test PRE-increment performance.
  sw.start();
  for(unsigned i = 0; i < 50000000; ++i)
  {
    ++PRE;
  }
  sw.stop();
  cout<<endl << "Final time PRE-INCR (ms): " << sw.get_final_time()/10000 <<endl;
  
  //Reset the stopwatch and create a new object for POST test.
  sw.reset();
  T POST(12.01);

  //Test POST-increment performance.
  sw.start();
  for(unsigned i = 0; i < 50000000; ++i)
  {
    POST++;
  }
  sw.stop();
  cout<<endl << "Final time POST-INCR (ms): " << sw.get_final_time()/10000 <<endl;  
  
  //Freeze frame (optional).
  std::cin.get();
  return 0;
}
The Final Result On Screen



Not impressive huh? Only 35 mili seconds difference. Compiler settings with max optimization, G++ 3.3.1., 2.8 GHz, dual cmos, dual channel memory.

But then again, our class is very simple. Usually, if a copy constructor is really needed, then times increase rapidly.

The Tip Of This Week:

Always use pre-increment if it doesn't affect the correctness of your program.

As Usual, Some Homework

- In the previous tip, I provided a simple stopwatch class. Fetch it.
- Change our test class so it is a wrapper for a double*. In other words, make it an Array class for type double.
- Implement the Rule Of Three. See my tutorial about the Rule Of Three HERE in case you need some assistance.
- Implement both a post/pre-increment operator for our class which increases ALL the elements in the array by 1.
- Prove your class setup is basically alright.
- Test pre-increment operator performance.
- Test post-increment operator performance.

What can you say now about those two operator's performance?

THE END

THE RULES
  • You may ask my assistance any time.
  • You may request my version of the final code, but you *must* email me your implementation. I need proof you did your best.
  • Do *not* post key code fragments in this forum. Let others think for themselves.
  • You may use your own stopwatch library if you have one. Provide key details though. I am not interested in full source code of any stopwatches.

Click HERE for my email.
Good luck!

Reference used::
Exceptional C++, Herb Sutter
__________________

Last edited by Valmont; 04-12-2005 at 09:52 AM.
Valmont is offline   Reply With Quote