Click here to Skip to main content
15,882,017 members
Articles / Programming Languages / C++
Article

Faster C++ Operators

Rate me:
Please Sign up or sign in to vote.
4.50/5 (13 votes)
21 Apr 2010CPOL4 min read 42.4K   136   25   17
A method to eliminate redundant memory copying in C++ operators

Introduction

This article presents a method to eliminate redundant memory copying in C++ operators that return a new object instance.

Background

One thing that has always annoyed me about the operators in C++ classes is the propensity for generating redundant memory copying. This problem can become severe if the instance contains a lot of data, such as a big matrix.

Let's say you have a MyType operator+ ( const MyType& other ) in your class. The operator is required to return an instance of MyType, which is easy, You just declare one in the function body, initialize it, and return it.

But.. when you return that instance, it goes out of scope because it's local, so C++ has to copy the data that you already initialized with a call to the copy constructor. Your object has to be constructed again!
This is the redundant memory copy I refer to.

Finding a Solution

To circumvent this copying, your operator can return an instance as it's being constructed, like so:

C++
return MyType( SomeParameter ); 

That's sweet, now you got rid of the copy construction call, and you're on your merry way.. unless your class needs lots of data to initialize itself.

Let's say your class is a matrix class (simplified here):

C++
class Mat8x8 {
    double  M[ 8*8 ];
public:
    Mat8x8()                      { memset( M, 0, 64*sizeof(double) ); }
    Mat8x8( const Mat8x8& other ) { memcpy( M, other.M, 512 ); }

    double& operator() ( int row, int col ) { return M[ row*8 + col ]; }

    Mat8x8 operator + ( const Mat8x8& other );
};

Your operator+ has to add each of the elements in the right hand instance to the left hand instance, and return an initialized instance with the result. You think about it for a bit, and figure out you can calculate the result in a temp array and use a constructor that takes your array as input.

C++
Mat8x8::Mat8x8( const double* data )  { memcpy( M, data, 512 ); }

Mat8x8::Mat8x8 operator + ( const Mat8x8& other ) {
    double Sum[64];
    for( int i=0; i<64; ++i )
        Sum[i] = M[i] + other.M[i];
    return Mat8x8( Sum );
    }

Well, at least you don't have to construct your instance twice, but you're not past the copying of initialized data. You build your result in a memory block that has to be copied to the memory C++ decides to use for the object instance. What you really want is a way to build your result straight into the memory the compiler decides to use for your instance. But how can you do that? You can't compute the result because you don't know where to put it, and you can't get somewhere to put it because you don't have anything to pass to the c'tor.

Some people try to get around this by defining a placement 'new' for the class, so they can get the c'tor to build in the memory they want, but that will, of course, only work for dynamically allocated instances. There has to be some other way. Which brings us to 'My way'..
A good old function pointer!

The following code listing embodies the method, and should speak for itself. A special constructor takes a function pointer and pointers to the left hand and right hand operands. The callback function does the actual work of the operator, once C++ has decided where to put your object instance.

C++
class Mat8x8 {
    //
    // The callback function does the actual work for the operator.
    //
    typedef void (*PFnInitMat)( Mat8x8& Mat, void* pLHS, void* pRHS );
    //
    // The special constructor takes a function pointer
    // and pointers to the left hand and right hand operands.
    //
    Mat8x8( PFnInitMat Init, void* pLHS, void* pRHS );
public:
    union {
    double  M[8][8];
    double  A[ 64 ];
    };
    Mat8x8()                      { memset( A, 0, 64*sizeof(double) ); }
    Mat8x8( const Mat8x8& other ) { memcpy( A, other.A, 512 ); }

    Mat8x8  operator + ( const Mat8x8& rhs );
    Mat8x8  operator / ( double rhs );
    // : etc..
};

typedef Mat8x8* PMat8x8;
typedef double* pdouble;

// The special constructor just forwards the work to the callback function.
Mat8x8::Mat8x8( PFnInitMat Init, void* pLHS, void* pRHS )
{
    Init( *this, pLHS, pRHS );
}

// An "operation" callback to add two matrices.
void AddMat88( Mat8x8& Mat, void* pLHS, void* pRHS )
{
    // These two references are not necessary, but makes reading easier.
    Mat8x8& lhs = (Mat8x8&) *PMat8x8( pLHS );
    Mat8x8& rhs = (Mat8x8&) *PMat8x8( pRHS );

    for( int r=0; r < 8; ++r )
        for( int c=0; c < 8; ++c )
            Mat.M[r][c] = lhs.M[r][c] + rhs.M[r][c];
}

// An "operation" callback to divide a matrix by a scalar.
void DivMat88scalar( Mat8x8& Mat, void* pLHS, void* pRHS )
{
    for( int i=0; i < 64; ++i )
        Mat.A[i] = PMat8x8( pLHS )->A[i] / *pdouble( pRHS );
}

// The operators delegate the work via a function pointer.
Mat8x8 Mat8x8::operator + ( const Mat8x8& rhs )
{
    return Mat8x8( AddMat88, this, (void*)&rhs );
}

Mat8x8 Mat8x8::operator / ( double rhs )
{
    return Mat8x8( DivMat88scalar, this, (pdouble)&rhs );
}

Eureka!

No more moving of initialized data. All you pass are a few pointers. ;)
In your operator, you just call the special constructor, and pass it a function pointer and pointers to the left hand and right hand instances. C++ finds a block for your instance and calls your initializer, which now can build the result straight into the memory the compiler chose.

All the operators can use the same technique, you just write suitable operation functions that you can pass to the constructor.

Afterthoughts

You may think this a lot of doing to get rid of a little data transferring, but what if you're dealing with big matrices of tens, or hundreds, of kBytes? Or you're dealing with massive amounts of instances? Little things add up. I hope you will find this technique useful, and that your programs will get faster for it..
I will certainly keep using 'My way'.

Using the Code

To make this scheme typesafe, you can change the callback function to take typed pointers or references instead of void pointers, and use several different callback function types to handle different operand types. I just used void* for this example - they make programming flexible (under great responsibility).

On the other hand, you could have a common callback function signature that you use for many classes, and cast them as you want.. the choice is yours. The special constructor should definitely not be public though.

Points of Interest

It's a well known, and somewhat sad, fact that you cannot call virtual methods in C++ constructors. You could, however, use this same technique to solve some cases where your constructor cannot know in advance how to deal with some initializations.

History

  • Original publication, April 2010

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
Thailand Thailand
Finally retired after a multi-faceted career as a software developer, systems architect, and IT consultant. I've moved away from Europe, and now I live a quiet life in Thailand.

I've developed code for all levels of firm/software on IBM PC compatible machines. BIOS code, device drivers, hardware test routines, application framework libraries, relational databases, windows multimedia applications, and more.

Now I can pursue my private pet projects, some of which will likely show up here on the code project..

Comments and Discussions

 
GeneralMy vote of 2 Pin
Aescleal17-May-10 11:48
Aescleal17-May-10 11:48 
GeneralRe: My vote of 2 Pin
Love Nystrom12-Jun-10 6:17
Love Nystrom12-Jun-10 6:17 
GeneralRe: My vote of 2 [modified] Pin
Aescleal12-Jun-10 7:52
Aescleal12-Jun-10 7:52 
Questionc'tor means ? Pin
merrykid12-May-10 17:52
merrykid12-May-10 17:52 
AnswerRe: c'tor means ? Pin
Love Nystrom14-May-10 2:50
Love Nystrom14-May-10 2:50 
GeneralGood job! Pin
Stefan_Lang26-Apr-10 22:53
Stefan_Lang26-Apr-10 22:53 
GeneralRe: Good job! Pin
Love Nystrom29-Apr-10 4:57
Love Nystrom29-Apr-10 4:57 
GeneralUse VC 2010... Pin
are_all_nicks_taken_or_what26-Apr-10 1:20
are_all_nicks_taken_or_what26-Apr-10 1:20 
GeneralRe: Use VC 2010... Pin
Love Nystrom26-Apr-10 5:25
Love Nystrom26-Apr-10 5:25 
GeneralRe: Use VC 2010... Pin
wtwhite26-Apr-10 21:19
wtwhite26-Apr-10 21:19 
GeneralRe: Use VC 2010... Pin
Love Nystrom29-Apr-10 5:16
Love Nystrom29-Apr-10 5:16 
GeneralRe: Use VC 2010... Pin
dpisarciuc3-May-10 5:06
dpisarciuc3-May-10 5:06 
QuestionR-Value operators ? Pin
Alexandre GRANVAUD22-Apr-10 3:10
Alexandre GRANVAUD22-Apr-10 3:10 
GeneralNRVO and RVO Pin
Kevin Drzycimski21-Apr-10 23:36
Kevin Drzycimski21-Apr-10 23:36 
GeneralRe: NRVO and RVO Pin
Nemanja Trifunovic22-Apr-10 3:12
Nemanja Trifunovic22-Apr-10 3:12 
GeneralRe: NRVO and RVO Pin
Love Nystrom23-Apr-10 8:54
Love Nystrom23-Apr-10 8:54 
Yes, NRVO will deal with the fundamental issue,
but it's not supported by all compilers (e.g mine),
and there are situations where NRVO won't kick in.

The technique I present is totally generic,
so it can be used with any compiler at all.

I'm not sure what You mean by measurements.
If You mean performance for the method, You
can estimate it's usefulness by looking at
the data size of your object versus how much
data is passed by the method.

The method passes three pointers from the
return constructor call, that's 12 bytes
(on 32 bit arch), then the constructor
forwards two of those pointers, that's
another 8 bytes, and then there is the
return address for the initializer call,
that's another 4. 24 bytes total overhead.

If Your object contains less data than that,
You would be penalized for using this method,
but if You carry more than 24 bytes data
(3 doubles), You can gain proportionally.

I would not bother using it for less than
5-6 doubles. I currently use it for the 3x3
and 4x4 double precision matrices in my physics
simulation engine. They are super-streamlined
for speed.

// Love
GeneralRe: NRVO and RVO Pin
Ejaz26-Apr-10 18:25
Ejaz26-Apr-10 18:25 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.