|
Boost : |
From: Daniel Frey (d.frey_at_[hidden])
Date: 2003-06-22 06:56:38
I had a small time-out and will now try to catch up with the
discussion. Before commenting on other postings, I'll show the result
of a small benchmark. The code is attached, maybe you want to verify
and post the results for your compiler(s), too :)
MACRO:
Intel C++ 7.1: 0.37
GCC 2.95.3: 2.12
GCC 3.3: 2.15
VARIABLE:
Intel C++ 7.1: 0.37
GCC 2.95.3: 0.62
GCC 3.3: 0.61
FUNCTION:
Intel C++ 7.1: 0.37
GCC 2.95.3: 0.62
GCC 3.3: 0.61
TEMPLATE:
Intel C++ 7.1: 0.37
GCC 2.95.3: 1.36
GCC 3.3: 1.35
As you can see, good compilers like the Intel are not suffering from
any presentation scheme, the GCC (althought not a bad compiler) has
some problems. Even the best versions are still much slower than the
Intel's compiler code. The template does have some performance
penalties, but remember it is just a benchmark. In real code, I expect
the difference to be smaller, but this will depend on the use
scenario. The most direct approach (MACRO) is even worse than the
template-approach. Conclusion: The code I suggest (TEMPLATE) is not
bad in itself, nothing prevents a compiler from generating a good
result from it. This doesn't mean that all compilers do so.
Before commenting on the benchmarks, please also look at the code
attached and note that I am well aware of the fact that all benchmarks
are lying :o)
As a result, we might want to evaluate if users that really need
speed use fast compilers and if those compilers suffer from the
code. If someone uses the GCC and complains about speed, one might
suggest to change the compiler anyway.
As a next point, I'd like to clean up with some misunderstandings. In
the example I showed I included a small optimization for sin(pi/2). I
think that some people haven't really seen the comment that says:
"...to solve the naming dilemma...". I admit that sin(pi/2) was a
stupid example in this context. I did not intend to add optimizations
to catch stupid user code. If a user writes sin(pi/2) it's not my
fault that this will get evaluated. I expect the programmer to realize
that this resolves to 1. The point is, that there are other constants,
like sqrt(2), e^(pi/4) or 1/ln(phi). The naming dilemma is that you
have to find a name that all programmers can remember. In this
context, I think that most programmers will understand these "names"
for the constants:
sqrt(two)
pow(e,pi/four)
one/ln(phi) // or one/log(phi)?
better than
sqrt2
ePowQuarterPi // or ePowPiDivFour?
oneDivLnPhi
Sure the above terms need to resolve to a single identifier like the
classic approach, but there is a difference: We needn't communicate
the internal names to the user as part of the interface.
We may also want to add the negative of all constants as I guess it
will come up very often that a constant C is used in a formular as
-C. But than maybe it's not needed as it will be integrated to the
formular by changing a+(-C) to a-c or something. Anyway, just another
idea/option.
Now for the operators. Paul mentioned that users want to write:
float area = pi * r * r;
I agree to that. But I also think it's reasonable to expect a user to
write
template-based code like this:
template< typename T > T area( const T& r ) {
return pi * r * r;
}
Nothing wrong with that in my eyes. I added the operators to allow
some convenience and I don't think there will be much surprises. The
only thing the user has to remember is, that he should not assume a
constant to have a 'special' type. pi in my code is not a double or
long double. It's just an internal type that the user should not care
about. If the users uses pi in conjunction with another type, pi
behaves like that type. Or the user can static_cast pi to whatever
type he wants. But pi itself is not of a user-visible type. That
given, it's IMHO obvious that for this code:
float f;
double d;
pi * f * d;
the last line is not using pi as a double but as a float. Also, I
don't expect these mixed cases to appear very often. Most users will
probably write code using only one type. Maybe they will also use two
or more types, but they will not interfer in lots of places. Usually
this will cause conversions and thus slow down the code, so the places
where mixed arithmetics occur should be quite rare.
I don't think anyone should be surprised by the above conversion to a
float instead of a double. If you are, think about:
int i = 41;
int j = 42;
double d = 1234.56;
double result = i / j * d;
What do you expect result to be? And this problem is much worse than
the conversion "problem" or pi. The example code I attached (which now
works *at least* for the gcc 2.95.x and 3.x and the Intel C++ 7) also
contains another operator I forgot in the first example (+=, ...).
Some people tried to simplify the code I showed. Well, in all cases I
think they lost at least one feature and only a benchmark which shows
a significant difference would justify that (IMO).
Errors and debugging:
My code will give you a compile-time error message for pi * 2;, so
safety is ensured. The message itself will contain the right line
number that caused the error, but it'll be a bit complicated to read
the actual error message. If you are a real user, you will get used to
it soon, as people got used to STL error messages. I don't think it's
a bit problem, just a minor inconvenience. YMMV.
The same is true for debugging. It's just inconvenient, but not
wrong. Also, you debugger might be configurable to skip calls to
certain functions / function patterns, so this should ease the use of
the template approach. Of course this depends on the debugger you are
using and your skills to configure it.
I think there are more open points, but this post is already long
enough. :)
Regards, Daniel
Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk