As a game developer, I’m concerned with performance. Yes, we’re living in next-gen land, and there’s a lot of performance – but then somebody comes along and squeezes every last drop out of a platform you develop for, and you better do that too.
As such, I’m occasionally involved in discussions about the merits of using C++. As such, one topic that comes up in every single discussion is the ‘hidden cost’ of using C++. And the reply is inevitably “There’s no such thing! It’s all in the source code!”
Well, yes, it is. But let me explain what I mean by it.
In C, if I have a line of code like this:
- a = func(b,c);
I can take a rough guess at the cost of the function by the name of it. The only other part I need to have a mental model of the performance is the overhead involved. And in C, the cost of a function call is pretty fixed. The only ‘surprise’ that can happen is that it’s inline and thus faster than you expected.
Not so in C++. Is it a function call, a member function call, or is it an anonymous constructor? Are b and c implicitly invoking copy constructors for other classes as part of type coercion? Is that a normal assignment, or an assignment operator? Is there a cast operator involved?
And once I have answered those questions, I have to look at all the classes involved. If they have a non-empty destructor, cost is added. Should those destructors be virtual, more cost is added. Virtual cast operators? Add some more.
As the end result, your overhead can grow dramatically. Especially those virtual calls are quite costly. The total runtime of a loop can easily vary by 10x or more based on those parameters.
Of course, they are not really hidden – if I look at the source code, I can easily see them. The real hidden cost is that now, instead of looking at one piece of source – the function itself – I need to look at up to four different classes. Add possible ancestors to find out if a call is virtual.
That is the hidden cost. The mental model for a simple function call became incredibly large and complex, and every function call is potentially as complex. Which makes reasoning about performance a rather hard thing to do.
Worse, it makes profiling harder than necessary. All the type coercions that happen at the API level will show up as separate functions, not attributed to the callee, but the caller.
All that translates ultimately into either worse performance or longer development time. Neither one is something you like to hear about.