Connecting Tech Pros Worldwide Forums | Help | Site Map

unexpected abstraction penalty in C++

alex goldman
Guest
 
Posts: n/a
#1: Jul 23 '05
class c {
int x;
public:
inline c() : x(0) {}
inline c(int i) { x = i; }
inline operator const int& () const { return x; }
inline operator int& () { return x; }
};


If I use objects of type `c' as if they were int's, I see about 1.5x
slow-down compared to using regular int's (timed using GCC-3.3.4 -O3). Is
it reasonable to expect modern compilers to optimize this, so that there is
no abstraction penalty?

Alf P. Steinbach
Guest
 
Posts: n/a
#2: Jul 23 '05

re: unexpected abstraction penalty in C++


* alex goldman:[color=blue]
> class c {
> int x;
> public:
> inline c() : x(0) {}
> inline c(int i) { x = i; }
> inline operator const int& () const { return x; }
> inline operator int& () { return x; }
> };[/color]

'inline' is superflous here, because the functions are defined in-class
which makes the automatically 'inline'.

Second, 'inline' is at best only a hint.

To direct your compiler to optimize for speed, use your compiler's options
and/or non-standard language extensions.

[color=blue]
> If I use objects of type `c' as if they were int's, I see about 1.5x
> slow-down compared to using regular int's (timed using GCC-3.3.4 -O3). Is
> it reasonable to expect modern compilers to optimize this, so that there is
> no abstraction penalty?[/color]

Don't know.

With the following main program:

int main( int nArgs, char* arg[] )
{
int forceCode = atoi( arg[1] );

int x = 0x1234 + forceCode;
x += 0x1111;
std::cout << x << std::endl;

c y = 0x1234 + forceCode;
y += 0x1111;
std::cout << y << std::endl;
}

Visual C++ 7.1, optimize for speed or full optimization, emits a
single instruction for the initialization of and addition to 'x',

; int x = 0x1234 + forceCode;
; x += 0x1111;
lea edx,[esi+2345h]

in contrast to two instruction for the code using your class, 'y',

; c y = 0x1234 + forceCode;
lea eax,[esi+1234h]
; y += 0x1111;
add eax,1111h

I don't understand why the compiler can see the optimization for 'x'
but not for 'y', having gone so far as to represent them identically.

--
A: Because it messes up the order in which people normally read text.
Q: Why is it such a bad thing?
A: Top-posting.
Q: What is the most annoying thing on usenet and in e-mail?
Chris Theis
Guest
 
Posts: n/a
#3: Jul 23 '05

re: unexpected abstraction penalty in C++


Alf P. Steinbach wrote:[color=blue]
> * alex goldman:
>[color=green]
>>class c {
>> int x;
>>public:
>> inline c() : x(0) {}
>> inline c(int i) { x = i; }
>> inline operator const int& () const { return x; }
>> inline operator int& () { return x; }
>>};[/color]
>
>
> 'inline' is superflous here, because the functions are defined in-class
> which makes the automatically 'inline'.
>
> Second, 'inline' is at best only a hint.
>
> To direct your compiler to optimize for speed, use your compiler's options
> and/or non-standard language extensions.
>
>
>[color=green]
>>If I use objects of type `c' as if they were int's, I see about 1.5x
>>slow-down compared to using regular int's (timed using GCC-3.3.4 -O3). Is
>>it reasonable to expect modern compilers to optimize this, so that there is
>>no abstraction penalty?[/color]
>
>
> Don't know.
>
> With the following main program:
>
> int main( int nArgs, char* arg[] )
> {
> int forceCode = atoi( arg[1] );
>
> int x = 0x1234 + forceCode;
> x += 0x1111;
> std::cout << x << std::endl;
>
> c y = 0x1234 + forceCode;
> y += 0x1111;
> std::cout << y << std::endl;
> }
>
> Visual C++ 7.1, optimize for speed or full optimization, emits a
> single instruction for the initialization of and addition to 'x',
>
> ; int x = 0x1234 + forceCode;
> ; x += 0x1111;
> lea edx,[esi+2345h]
>
> in contrast to two instruction for the code using your class, 'y',
>
> ; c y = 0x1234 + forceCode;
> lea eax,[esi+1234h]
> ; y += 0x1111;
> add eax,1111h
>
> I don't understand why the compiler can see the optimization for 'x'
> but not for 'y', having gone so far as to represent them identically.
>[/color]

IMHO it's related to the point of time when the compiler's constant
folding kick in. I'd expect it to operate primarily on POD leaving out
objects (even though they might act the way the OP implements them). I
suppose this could impose overhead in the dataflow analysis which
probably won't pay off in most cases.

Cheers
Chris
Ron Natalie
Guest
 
Posts: n/a
#4: Jul 23 '05

re: unexpected abstraction penalty in C++


alex goldman wrote:[color=blue]
> class c {
> int x;
> public:
> inline c() : x(0) {}
> inline c(int i) { x = i; }
> inline operator const int& () const { return x; }
> inline operator int& () { return x; }
> };
>
>
> If I use objects of type `c' as if they were int's, I see about 1.5x
> slow-down compared to using regular int's (timed using GCC-3.3.4 -O3). Is
> it reasonable to expect modern compilers to optimize this, so that there is
> no abstraction penalty?[/color]

Well you don't show us your code that actually uses the class, but there
are a few differences between
int [100];
and
c [100];

The first is that with an array of ints (or any POD) the default
initialization is either static or omitted. In your case,
a dynamic initialization always occurs.
Donovan Rebbechi
Guest
 
Posts: n/a
#5: Jul 23 '05

re: unexpected abstraction penalty in C++


On 2005-05-29, alex goldman <hello@spamm.er> wrote:[color=blue]
> class c {
> int x;
> public:
> inline c() : x(0) {}
> inline c(int i) { x = i; }
> inline operator const int& () const { return x; }
> inline operator int& () { return x; }
> };
>
>
> If I use objects of type `c' as if they were int's, I see about 1.5x
> slow-down compared to using regular int's (timed using GCC-3.3.4 -O3). Is
> it reasonable to expect modern compilers to optimize this, so that there is
> no abstraction penalty?[/color]

Does it make any difference if you change that to operator int() ?

Cheers,
--
Donovan Rebbechi
http://pegasus.rutgers.edu/~elflord/
alex goldman
Guest
 
Posts: n/a
#6: Jul 23 '05

re: unexpected abstraction penalty in C++


Ron Natalie wrote:
[color=blue]
> In your case, a dynamic initialization always occurs.[/color]

That's a very good point. I removed the initialization just to be sure, but
it didn't make a difference for speed. It's still 2:3 for int vs the class.

Here's one of the benchmarks I ran:

#include <iostream>

class c {
int x;
public:
c() {}
c(int i) { x = i; }
operator const int& () const { return x; }
operator int& () { return x; }
};

#define LOOP1(i, n) for((i) = -(n); (i) <= (n); ++(i))
#define LOOP(i, j, k, n, a) \
a = 0; \
LOOP1(i, n) LOOP1(j, n) LOOP1(k, n) \
a += k + j; return a

int f_i(int n) {
int i, j, k, acc;
LOOP(i, j, k, n, acc);
}

int f_c(c n) {
c i, j, k, acc;
LOOP(i, j, k, n, acc);
}

int main() {
// std::cout << f_i(1000) << '\n';
std::cout << f_c(1000) << '\n';
}

alex goldman
Guest
 
Posts: n/a
#7: Jul 23 '05

re: unexpected abstraction penalty in C++


Donovan Rebbechi wrote:
[color=blue]
>
> Does it make any difference if you change that to operator int() ?
>[/color]

I can't change the non-const operator to int (), but changing the const one
makes no difference.
Ron Natalie
Guest
 
Posts: n/a
#8: Jul 23 '05

re: unexpected abstraction penalty in C++


alex goldman wrote:[color=blue]
> Ron Natalie wrote:
>
>[color=green]
>> In your case, a dynamic initialization always occurs.[/color]
>
>
> That's a very good point. I removed the initialization just to be sure, but
> it didn't make a difference for speed. It's still 2:3 for int vs the class.
>[/color]
You might try adding operator += and ++ to see what difference that
makes, or alternatively a operator=(int).
alex goldman
Guest
 
Posts: n/a
#9: Jul 23 '05

re: unexpected abstraction penalty in C++


I wrote a more comprehensive test of various abstraction penalties in C++.
Here's what I get on P4 with GCC-3.3.4 -O3 (agressive optimization &
inlining):

$ time ./a.out

f_int(1000) : 9.240s
f_class1(1000) : 13.890s
f_class2(1000) : 19.510s
f_method(1000) : 13.850s
f_macro(1000) : 9.320s
f<int>(1000) : 9.240s
f<c>(1000) : 19.490s
f_get1(1000) : 13.850s
f_get2(1000) : 32.660s

real 2m21.092s
user 2m20.928s
sys 0m0.133s


Lessons learned:
* regular accessors (getters & setters) didn't help
* very minor things can confuse the optimizer (class1 vs class2)

I'm be curious to know how other CPUs/compilers do. Program text follows.



#include <iostream>
#include <ctime>
#include <iomanip>

using namespace std;

double time() { return double(clock()) / CLOCKS_PER_SEC; }

#define TIME_INC(e, res) { \
double t1 = time(); \
(res) += (e); \
double t2 = time(); \
cout << setw(15) << #e << " : " \
<< setw(7) << fixed << setprecision(3) \
<< t2 - t1 << "s" << endl; \
}

class c {
public:
int x;
c() {}
c(int i) : x(i) {}
operator const int& () const { return x; }
operator int& () { return x; }
const int& i() const { return x; }
int& i() { return x; }
int get() const { return x; }
void set(int i) { x = i; }
};

#define LOOP1(i, n) for((i) = -(n); (i) <= (n); ++(i))
#define LOOP(i, j, k, n, a) \
a = 0; \
LOOP1(i, n) LOOP1(j, n) LOOP1(k, n) \
a += k + j; return a

int f_int(int n) {
int i, j, k, acc;
LOOP(i, j, k, n, acc);
}

int f_class1(c n) {
c i, j, k, acc;
LOOP(i, j, k, n, acc);
}

// the return type is different from the above

c f_class2(c n) {
c i, j, k, acc;
LOOP(i, j, k, n, acc);
}

int f_method(c n) {
c i, j, k, acc;
LOOP(i.i(), j.i(), k.i(), n.i(), acc.i());
}

// very similar, but 1.5x faster!

#define I(e) (e).x
int f_macro(c n) {
c i, j, k, acc;
LOOP(I(i), I(j), I(k), I(n), I(acc));
}

template<class T>
T f(T n) {
T i, j, k, acc;
LOOP(i, j, k, n, acc);
}

int f_get1(c n) {
c i, j, k, acc = 0;
for(i.set(-n.get()); i.get() <= n.get(); i.set(i.get() + 1))
for(j.set(-n.get()); j.get() <= n.get(); j.set(j.get() + 1))
for(k.set(-n.get()); k.get() <= n.get(); k.set(k.get() + 1))
acc.set(acc.get() + k.get() + j.get());
return acc;
}

// the return type makes a big difference:

c f_get2(c n) {
c i, j, k, acc = 0;
for(i.set(-n.get()); i.get() <= n.get(); i.set(i.get() + 1))
for(j.set(-n.get()); j.get() <= n.get(); j.set(j.get() + 1))
for(k.set(-n.get()); k.get() <= n.get(); k.set(k.get() + 1))
acc.set(acc.get() + k.get() + j.get());
return acc;
}

#define TIME(e) TIME_INC(e(1000), dummy)

int main() {
int dummy = 0;
TIME(f_int);
TIME(f_class1)
TIME(f_class2)
TIME(f_method);
TIME(f_macro);
TIME(f<int>);
TIME(f<c>);
TIME(f_get1);
TIME(f_get2);
return dummy;
}

Closed Thread