[comp.lang.c++] why is this program slow?

shirley@iuvax.cs.indiana.edu (peter shirley) (01/09/91)

I have a C++ program that takes about 170% as long as a similar C program.
I've run it on a vax and a sun with and without big-O.
I've inlined everything I can, and I don't understand the slowdown.
Could someone enlighten me?  (yes, I looked at the C code generated
by CC, and am too dumb to understand it).

The C, then C++ code follow.

Thanks,

Pete Shirley
shirley@cs.indiana.edu

**************** begin C code *********************

#include "stdio.h"


main()
{
    int i;
    double a[3], b[3]; 

    a[0] = a[1] = a[2] = 0.0;
    b[0] = 1.0;
    b[1] = 2.0;
    b[2] = 4.0;

    for ( i = 0; i < 1000000; i++)
    {
        a[0] = a[0] + b[0];
        a[1] = a[1] + b[1];
        a[2] = a[2] + b[2];
    }

    fprintf(stderr, "%lf %lf %lf\n", a[0], a[1], a[2]);
}


************* end C code *******************

************** begin C++ code *********************

#include <math.h>
#include <stream.h>


class vector {
protected:
    double data[3];
public:
    vector();
    vector(double, double, double);
    void set(double, double, double);
    void operator=(vector&);
    friend ostream& operator<<(ostream&, vector&);
    friend vector operator+(vector&, vector&);
};


inline vector::vector()
{
}


inline vector::vector(double a, double b, double c)
{
   data[0] = a;  data[1] = b;  data[2] = c;
}


inline void vector::set(double a, double b, double c)
{
   data[0] = a;  data[1] = b;  data[2] = c;
}

inline void vector::operator=(vector& v)
{

   data[0] = v.data[0];
   data[1] = v.data[1];
   data[2] = v.data[2];

}



inline vector operator+(vector& u, vector& v)
{
    vector temp;

    temp.data[0] = u.data[0] + v.data[0];
    temp.data[1] = u.data[1] + v.data[1];
    temp.data[2] = u.data[2] + v.data[2];
    return temp;
}

inline ostream& operator<<(ostream& s, vector& t)
{
   return s << "(" << t.data[0] << ", "<< t.data[1] <<", " << t.data[2] <<")";
}


main()
{
    vector a(0.0, 0.0, 0.0);
    vector b(1.0, 2.0, 4.0);

    for (int i = 0; i < 1000000; i++)
        a = a + b;

    cerr << a << "\n";
}

************** end C++ code ***********************

shirley@iuvax.cs.indiana.edu (peter shirley) (01/09/91)

Several people on this group and by mail suggested I needed a +=
operation for vectors.  I tried this, and on our vax it got the C++
code to be as fast as the C code.  On a sparc, it was as fast with "big-O"
turned off, but the C code was much better with the optimizer on.  Maybe
cc was smart enough to change something like:

	a = 0;
	for (i=0; i < 1000000; i++)
	   a = a + 1;

to:

	a = 1000000;


Anyway, things are working great now.  Thank you all for the quick help!

pete shirley

jak@cs.brown.edu (Jak Kirman) (01/09/91)

In article <1991Jan9.002244.23398@news.cs.indiana.edu> shirley@iuvax.cs.indiana.edu (peter shirley) writes:


   I have a C++ program that takes about 170% as long as a similar C program.
   I've run it on a vax and a sun with and without big-O.
   I've inlined everything I can, and I don't understand the slowdown.
   Could someone enlighten me?  (yes, I looked at the C code generated
   by CC, and am too dumb to understand it).

   [code using a vector class, adding vectors with operator+]

Part of the problem at least is that you are using a function which
returns an object.  Typically what happens in this case (at least under
cfront) is that the function is called, the return value copied into a
temporary, and then the assignment function is called to copy the
temporary into the destination.  So with vector a, b, and a + operator
which returns a vector,

        a = a + b;

will result in a call to the + operator, which returns a vector.  This
vector is copied into the temporary by C, and then the assignment
operator is called.  So you are doing nearly twice the work.

With a += operator, changing that line to

        a += b

the time was about the same without optimization on a Sparc with AT&T
2.0.  The time was still substantially faster in C with optimization; I
am not sure what optimizations are being performed in C but not C++.

Ellis & Stroustrup ARM 12.1.1c provides an explanation of why the
optimizations necessary to avoid the extra copying would be very
difficult for most compilers, and points out that in the case of
initialization it often can be optimized.

Below is a simple example of C++ code and a cleaned-up version of what is
produced by cfront.  I have changed the names of the temporaries and
unmangled the names, and performed a few trivial "simplifications" of
the code, such as changing (a,b) to a;b where the result of (a,b) is not
used.  In the example, when the result of foo is assigned to an existing
X object, a temporary is created.  When the result of foo is used to
initialize an X object, no temporary is created.

Note that things would be different were there a copy constructor for X;
the function foo would be changed to take a pointer to the location
where the result should be created.

struct X {
  int a;
  int b;
  void operator= (const X& o) { a = o.a; b = o.b; }
};


X foo ()
{
  X myx;
  return myx;
}

main ()
{
  X x1;

  x1 = foo ();
  X x2 = foo ();
}


struct X {
int a ;
int b ;
};

struct X foo ()
{ 
  struct X myx ;
  
  return myx ;          /* note that C returns a structure */
}


int main ()
{
  _main(); 
  { 
    struct X x1 ;
    struct X x2 ;
    struct X *p ;
    { 
      struct X tmp ;
      tmp = foo ();             /* structure copied into tmp */
      p = &tmp;
      
      (& x1 )-> a = *p.a ;      /* operator= used to copy tmp into x1 */
      (& x1 )-> b = *p.b ;
      
      x2 = foo ( ) ;            /* no copy necessary
    }
  }
} 

ggs@ulysses.att.com (Griff Smith) (01/09/91)

In article <1991Jan9.002244.23398@news.cs.indiana.edu>, shirley@iuvax.cs.indiana.edu (peter shirley) writes:
> I have a C++ program that takes about 170% as long as a similar C program.
> I've run it on a vax and a sun with and without big-O.
> I've inlined everything I can, and I don't understand the slowdown.

The C program is written in terms of basic operations that the optimizer
can easily improve; the C++ version isn't.  For example:

>         a[0] = a[0] + b[0];

This is better written as

	  a[0] += b[0];

though modern compilers can usually make the two cases equivalent.  The
C++ version is not so fortunate.  You write

>         a = a + b;

Since `a' is a structure, your `+' operator first has to store the sum in
your `temp', then your `=' operator must copy it into `a'.  This takes a
string move at best, and a slow function call at worst.  I tried redefining
your `+' operator to be a `vector& +=' operator and my execution time on
a tahoe became 6.0 seconds instead of 14.5.  That's still not quite as
good as 4.5 for the C version, but it explains most of the difference.

> Could someone enlighten me?  (yes, I looked at the C code generated
> by CC, and am too dumb to understand it).

C++ output isn't intelligible to most mortals; this mortal frequently
has trouble with C++ input.  I often find it easier to use adb to look
at the assembly language.
-- 
Griff Smith	AT&T (Bell Laboratories), Murray Hill
Phone:		1-201-582-7736
UUCP:		{most AT&T sites}!ulysses!ggs
Internet:	ggs@ulysses.att.com

steve@Pkg.Mcc.COM (Steve Madere) (01/10/91)

In article <1991Jan9.002244.23398@news.cs.indiana.edu>,
shirley@iuvax.cs.indiana.edu (peter shirley) writes:
| I have a C++ program that takes about 170% as long as a similar C
program.
| I've run it on a vax and a sun with and without big-O.
| I've inlined everything I can, and I don't understand the slowdown.
| Could someone enlighten me?  (yes, I looked at the C code generated
| by CC, and am too dumb to understand it).
| 

<Stuff deleted>
<C program segment folows>
|     for ( i = 0; i < 1000000; i++)
|     {
|         a[0] = a[0] + b[0];
|         a[1] = a[1] + b[1];
|         a[2] = a[2] + b[2];
|     }

<C++ program segment follows>
| inline void vector::operator=(vector& v)
| {
| 
|    data[0] = v.data[0];
|    data[1] = v.data[1];
|    data[2] = v.data[2];
| 
| }
| 
| 
| 
| inline vector operator+(vector& u, vector& v)
| {
|     vector temp;
| 
|     temp.data[0] = u.data[0] + v.data[0];
|     temp.data[1] = u.data[1] + v.data[1];
|     temp.data[2] = u.data[2] + v.data[2];
|     return temp;
| }

<Stuff deleted>
|     for (int i = 0; i < 1000000; i++)
|         a = a + b;
| 
|     cerr << a << "\n";
| }

At this point the cause of the speed problems should be obvious.
Your C++ program is not quite equivalent to your C program.
To make them equivalent, you should have the following C program
segment substituted for the one above.

for (i=0;i<1000000;i++)
{
  temp = calloc(3,sizeof(*temp));
  temp[0] = a[0] + b[0];
  temp[1] = a[1] + b[1];
  temp[2] = a[2] + b[2];
  a[0]=temp[0];
  a[1]=temp[1];
  a[2]=temp[2];
  free(temp);
}

Once this is done, I think your two programs will probably run
at the same speed :-(.

This is a problem with C++ that I have known about for some time.
The lure of simplified notation leads one to try to implement
vector and matrix operations with special classes that will hide
the complexity.  Unfortunately, all of those temporary variables
that must be created to realize any mathematical operations really
waste CPU time.  It takes a finite amount of time to allocate space,
copy data, and free up the space again.  In many cases it is a
significant percentage of the total run time of the program.

A friend of mine at UCSD has worked on this problem for some time
and concluded that what one needs is some kind of matrix awareness
in the compiler to recognize when operations can be somehow 
combined.  Clearly this is not the kind of thing that C++ was designed
to do.  It is appropriate to add matrix awareness to a computational
language like FORTRAN but C++ is not a scientific computation
language.  (no flames please, I just mean that its PRIMARY purpose
is not scientific computation).

Maybe what one needs is a preprocessed language that takes matrix
operations, turns them into optimized C code which you can 
link with your C++ program.  

Any suggestions from the peanut gallery?

Steve Madere
steve@pkg.mcc.com

steve@taumet.com (Stephen Clamage) (01/10/91)

shirley@iuvax.cs.indiana.edu (peter shirley) writes:

>I have a C++ program that takes about 170% as long as a similar C program.
>I've run it on a vax and a sun with and without big-O.
>I've inlined everything I can, and I don't understand the slowdown.

>inline vector operator+(vector& u, vector& v)
>{
>    vector temp;
>    temp.data[0] = u.data[0] + v.data[0];
>    temp.data[1] = u.data[1] + v.data[1];
>    temp.data[2] = u.data[2] + v.data[2];
>    return temp;
>}

This is your problem.  You create a local object, then return it by
value.  This must then be copied into the target variable.  The
equivalent C code looks like this:

typedef struct { double data[3]; } vector;

vector plus( vector* this, vector* u, vector* v)
{
    ...the body is the same as above...
}

main()
{
    vector u, v, w;
    ...
    w = plus(&w, &u, &v);
    ...
}

Compare
    w = plus(&w, &u, &v);
to
    w.data[0] = u.data[0] + v.data[0];
    w.data[1] = u.data[1] + v.data[1];
    w.data[2] = u.data[2] + v.data[2];
There is an extra structure copy using plus() instead of direct code,
and the copy is in the inner loop.

Try rewriting operator+ as:

inline vector operator+(vector& u, vector& v)
{
    return vector(u.data[0] + v.data[0],
		  u.data[1] + v.data[1],
		  u.data[2] + v.data[2]);
}

Many C++ compilers are smart enough not to create a temporary object,
but to construct the new vector directly in the target variable.
-- 

Steve Clamage, TauMetric Corp, steve@taumet.com

vaughan@mcc.com (Paul Vaughan) (01/10/91)

   From: shirley@iuvax.cs.indiana.edu (peter shirley)
   Newsgroups: comp.lang.c++
   Date: 9 Jan 91 05:22:13 GMT
   Organization: Computer Science, Indiana University
   Lines: 115

   I have a C++ program that takes about 170% as long as a similar C program.
   I've run it on a vax and a sun with and without big-O.
   I've inlined everything I can, and I don't understand the slowdown.
   Could someone enlighten me?  (yes, I looked at the C code generated
   by CC, and am too dumb to understand it).

The answer is that the C++ version has to construct and copy a
temporary vector.  Here are some timings I made that show this effect
by noting the difference between using your version and using a
vector::operator+= that avoids the copy.  

These timings were all on a
SUN4.  Note the user time.

Using a = a + b
[1.95]sunspot) g++-1.90.04 -O -o trash trash.cc -lg++
trash.cc: In function class vector operator + (class vector &, class vector &):
trash.cc:62: warning: bitwise copy: `vector' defines operator=() 
[1.96]sunspot) /bin/time trash
(1e+06, 2e+06, 4e+06)
       32.1 real        10.0 user         0.2 sys 

(Ignore the warning from g++, it's inappropriate)

Using a += b;
[1.93]sunspot) g++-1.90.04 -O -o trash trash.cc -lg++
trash.cc: In function class vector operator + (class vector &, class vector &):
trash.cc:62: warning: bitwise copy: `vector' defines operator=()
[1.94]sunspot) /bin/time trash
(1e+06, 2e+06, 4e+06)
       13.3 real         5.2 user         0.0 sys  


Using a = a + b;
[1.97]sunspot) CC -O -o trash trash.cc
CC  trash.cc:
cc   -o /d2/prism/prism/src/sim/src/trash -I/net/sunspot/usr/cadsw/CC/sun4/incl  -O trash.c  -L/net/sunspot/usr/cadsw/CC/sun4/ -lC
[1.98]sunspot) /bin/time trash
(1e+06, 2e+06, 4e+06)
       23.0 real         8.2 user         0.1 sys  

Using a += b;
[1.99]sunspot) CC -O -o trash trash.cc
CC  trash.cc:
cc   -o /d2/prism/prism/src/sim/src/trash -I/net/sunspot/usr/cadsw/CC/sun4/incl  -O trash.c  -L/net/sunspot/usr/cadsw/CC/sun4/ -lC
[1.100]sunspot) /bin/time trash
(1e+06, 2e+06, 4e+06)
        8.8 real         3.4 user         0.1 sys  

Here is the vector::operator+= that I added:


inline vector& vector::operator+=(vector& v)
{

   data[0] += v.data[0];
   data[1] += v.data[1];
   data[2] += v.data[2];
   return *this;
}

The concept of a named return value that a compiler could use to
optimize away this sort of temporary copy has been proposed.  I think
it was Doug Lea who came up with that, but I'm not sure.  I don't know
the status of that suggestion.

Finally, here is the original code.


   The C, then C++ code follow.

   Thanks,

   Pete Shirley
   shirley@cs.indiana.edu

   **************** begin C code *********************

   #include "stdio.h"


   main()
   {
       int i;
       double a[3], b[3]; 

       a[0] = a[1] = a[2] = 0.0;
       b[0] = 1.0;
       b[1] = 2.0;
       b[2] = 4.0;

       for ( i = 0; i < 1000000; i++)
       {
	   a[0] = a[0] + b[0];
	   a[1] = a[1] + b[1];
	   a[2] = a[2] + b[2];
       }

       fprintf(stderr, "%lf %lf %lf\n", a[0], a[1], a[2]);
   }


   ************* end C code *******************

   ************** begin C++ code *********************

   #include <math.h>
   #include <stream.h>


   class vector {
   protected:
       double data[3];
   public:
       vector();
       vector(double, double, double);
       void set(double, double, double);
       void operator=(vector&);
       friend ostream& operator<<(ostream&, vector&);
       friend vector operator+(vector&, vector&);
   };


   inline vector::vector()
   {
   }


   inline vector::vector(double a, double b, double c)
   {
      data[0] = a;  data[1] = b;  data[2] = c;
   }


   inline void vector::set(double a, double b, double c)
   {
      data[0] = a;  data[1] = b;  data[2] = c;
   }

   inline void vector::operator=(vector& v)
   {

      data[0] = v.data[0];
      data[1] = v.data[1];
      data[2] = v.data[2];

   }



   inline vector operator+(vector& u, vector& v)
   {
       vector temp;

       temp.data[0] = u.data[0] + v.data[0];
       temp.data[1] = u.data[1] + v.data[1];
       temp.data[2] = u.data[2] + v.data[2];
       return temp;
   }

   inline ostream& operator<<(ostream& s, vector& t)
   {
      return s << "(" << t.data[0] << ", "<< t.data[1] <<", " << t.data[2] <<")";
   }


   main()
   {
       vector a(0.0, 0.0, 0.0);
       vector b(1.0, 2.0, 4.0);

       for (int i = 0; i < 1000000; i++)
	   a = a + b;

       cerr << a << "\n";
   }

   ************** end C++ code ***********************
-- 
 Paul Vaughan, MCC CAD Program | ARPA: vaughan@mcc.com | Phone: [512] 338-3639
 Box 200195, Austin, TX 78720  | UUCP: ...!cs.utexas.edu!milano!cadillac!vaughan
----------------------------------------------------------------------------------
I spent from $3 to $10 today to pay interest on the national debt.  How about you?
----------------------------------------------------------------------------------


 Paul Vaughan, MCC CAD Program | ARPA: vaughan@mcc.com | Phone: [512] 338-3639
 Box 200195, Austin, TX 78720  | UUCP: ...!cs.utexas.edu!milano!cadillac!vaughan
----------------------------------------------------------------------------------

jimad@microsoft.UUCP (Jim ADCOCK) (01/11/91)

In article <1991Jan9.101212@Pkg.Mcc.COM> steve@Pkg.Mcc.COM (Steve Madere) writes
|This is a problem with C++ that I have known about for some time.
|The lure of simplified notation leads one to try to implement
|vector and matrix operations with special classes that will hide
|the complexity.  Unfortunately, all of those temporary variables
|that must be created to realize any mathematical operations really
|waste CPU time.  It takes a finite amount of time to allocate space,
|copy data, and free up the space again.  In many cases it is a
|significant percentage of the total run time of the program.
|
|A friend of mine at UCSD has worked on this problem for some time
|and concluded that what one needs is some kind of matrix awareness
|in the compiler to recognize when operations can be somehow 
|combined.  Clearly this is not the kind of thing that C++ was designed
|to do.  It is appropriate to add matrix awareness to a computational
|language like FORTRAN but C++ is not a scientific computation
|language.  (no flames please, I just mean that its PRIMARY purpose
|is not scientific computation).
|
|Any suggestions from the peanut gallery?

My claim would be that if one is serious about working with large objects --
say large matrices and/or large vectors, then one can afford to 
"do the job right" in C++ -- which means probably using reference counting
on the internal storage of the vectors and/or matrices, while maintaining
an external polymorphic wrapper over those guts which represents the 
exact form of the vector or matrix: is this matrix a "plain old matrix",
tridiagonal, identity, .... 

[In fact, probably some vendors already provide such libraries]

If you have a parallel computer, and a highly parallelizing fortran compiler,
you might want to write some of the guts of the internal representation of
these matrices in fortran -- but better yet, you'll probably find that 
your vendor already provides highly optimized routines for performing
those guts -- linpack-type stuff.

There's no reason why you can't have both the nice syntax of C++, and
the speed of fortran -- if you're willing to do a little serious work.
On the other hand, if you don't care, well, then you don't care.

Bruce.Hoult@bbs.actrix.gen.nz (01/11/91)

Peter Shirley writes:
>I have a C++ program that takes about 170% as long as a similar C program.
>I've run it on a vax and a sun with and without big-O.
>I've inlined everything I can, and I don't understand the slowdown.
>Could someone enlighten me?  (yes, I looked at the C code generated
>by CC, and am too dumb to understand it).
 
 
I get the same thing on my Mac IIcx: the C program takes 31 seconds, and
the C++ one takes 54.
 
The problem is that the compiler isn't all that smart and is (on my
system, and probably on yours) copying the result of operator+ twice
on the way to its final destination -- the first time using a bitwise
copy (the default copy constructor), and the second time using the
operator= that you defined.
 
One way to work around the problem is to define an operator+=.  This will
drop the execution time down to virtually the same as for the C program.

Another way is to put the output of CFront through a smarter C compiler. I
suspect that GCC would do a rather better job of optomising the mess that
CFront leaves behind.
-- 
Bruce.Hoult@bbs.actrix.gen.nz   Twisted pair: +64 4 772 116
BIX: brucehoult                 Last Resort:  PO Box 4145 Wellington, NZ

mat@mole-end.UUCP (Mark A Terribile) (01/11/91)

> > I have a C++ program that takes about 170% as long as a similar C program.
> > ...  I don't understand the slowdown.

> The C program is written in terms of basic operations that the optimizer
> can easily improve; the C++ version isn't.  For example:
 
> >         a[0] = a[0] + b[0];
> This is better written as
> 	  a[0] += b[0];
 
> though modern compilers can usually make the two cases equivalent.  The
> C++ version is not so fortunate.  You write
 
> >         a = a + b;
 
> Since `a' is a structure, your `+' operator first has to store the sum in
> your `temp', then your `=' operator must copy it into `a'.  This takes a
> string move at best, and a slow function call at worst.  I tried redefining
> your `+' operator to be a `vector& +=' operator and my execution time on
> a tahoe became 6.0 seconds instead of 14.5.  That's still not quite as
> good as 4.5 for the C version, but it explains most of the difference.
 
There's also some genuinely bad use of C++ initialization rules around
returns and parameters.  Fixing it won't make the problem go away, but it
will probably shave about half off the extra cost.  In the operator+() , you
force the loading of a temporary vector, then a copy from the temporary to
the return value.  Sometimes this is unavoidable; here it is not.
Instead of writing

	inline vector operator+(vector& u, vector& v)
	{
	    vector temp;

	    temp.data[0] = u.data[0] + v.data[0];
	    temp.data[1] = u.data[1] + v.data[1];
	    temp.data[2] = u.data[2] + v.data[2];
	    return temp;
	}

write

	inline vector operator+(vector& u, vector& v)
	{
	    return vector( u.data[0] + v.data[0], u.data[1] + v.data[1],
						    u.data[2] + v.data[2] );
	}

There is no absolute guarantee that the compiler will not build a temporary
and do the copy, but at least some, under at least some circumstances,
will build the return value in the place in which it is to finally rest.
Class/struct return is usually implemented, and especially in cfront, by
passing a pointer to a place wherein the return value is to be built, or by
providing extra stack space for the return value.  In either case, writing
the `value builder' constructor in the return statement gives the compiler
permission to optimize away the copy constructor invocation by building the
result in place.


> > Could someone enlighten me?  (yes, I looked at the C code generated
> > by CC, and am too dumb to understand it).

It's not that bad.  Use a good editor to match up the parens and split the
lines to exhibit the parentheses nesting, then remove truly redundant parens.
Remember that `;' becomes `,' and if/then/else is implemented with  ?: .  A
`0' is used to represent a null-else.

Most of the rest of the opacity comes from name mangling and the nested-
variable prefixes.  Those you will get the hang of in time.
-- 

 (This man's opinions are his own.)
 From mole-end				Mark Terribile

jbuck@galileo.berkeley.edu (Joe Buck) (01/16/91)

In article <1877@ruunsa.fys.ruu.nl>, muts@fysaj.fys.ruu.nl (Peter Mutsaers /1000000) writes:
|> It is a pity that the c++ optimizer does not (yet) automatically
|> change a = a + b into a+=b if operator+= is defined too. It would
|> reduce however the freedom to use operators like += *= etc.

I got this trick from Andy Koenig.

Whenever I write a class with arithmetic operators, I always write
+=, *=, etc, as member functions and then write (let's say the class
is Foo)

inline Foo operator+ (const Foo& a, const Foo& b) {
	Foo tmp(a);
	tmp += b;
	return tmp;
}

Notice that this function does not need to be a friend (so when
people ask me whether arithmetic operators should be members or
friends, I reply, neither).  With this kind of structure, most
optimizers can convert

func (Foo& x, Foo& y) {
	...

	Foo c = x + y;
	...
}

into
	Foo c(x); c += y;

with little trouble.
--
Joe Buck
jbuck@galileo.berkeley.edu	 {uunet,ucbvax}!galileo.berkeley.edu!jbuck