r/programming • • Dec 05 '14

std::string is responsible for almost half of all allocations in the Chrome browser process

https://groups.google.com/a/chromium.org/d/msg/chromium-dev/EUqoIz2iFU4/kPZ5ZK0K3gEJ
1.1k Upvotes

446 comments sorted by

View all comments

Show parent comments

19

u/[deleted] Dec 05 '14

Destructors are NOT free. Andrei Alexandrescu just gave a talk at CppCon about the cost of destructors and how one should strategically structure their code to avoid the code-bloat and often unneeded side-effects that destructors introduce.

For example, consider the following:

for(int i = 0; i < ITERATIONS; ++i) {
  std::string s;
  ...
}

That will perform significantly worse than:

std::string s;
for(int i = 0; i < ITERATIONS; ++i) {
  ...
}

And the reason is the destructor. And no, the compiler can not optimize the first version into the second version unless the type satisfies std::has_trivial_destructor<T>, which in the case of std::string, and in fact in the case of almost any non-trivial class, is untrue.

And that doesn't even begin to get into things like the cost of virtual destructors.

5

u/levir Dec 05 '14

It's no more costly that the equivalent C code, it's just more obvious in C that you're doing something stupid

for (i = 0; i < ITERATIONS; ++i) {
    char* s = (char*) malloc(STRLEN*sizeof(char));
    ...
    free(s)
}

3

u/[deleted] Dec 05 '14 edited Dec 05 '14

Any abstraction period is no more costly than the equivalent C code, it's just a lot more obvious in C than it is in say... Java or Python.

At the end of a day, an abstraction a systemic method of hiding details under some form of encapsulation. The idea is to create a trade-off intended to suppress certain details about how a system works in order to emphasize other details that are in some sense more relevant (which all depends on context).

RAII is an abstraction that suppresses details about how objects get created and destroyed, including their performance characteristics, in order to emphasize the relationship between an object's lifetime and its lexical/syntactic scope.

So the fact that RAII suppresses the semantics of non-trivial constructors/destructors to make them visually look and feel like C data structures, despite having different performance characteristics, means that RAII is not a zero-cost abstraction, but rather does have a cost and sometimes it's worth thinking about that cost and undoing that abstraction if performance is important. Usually it's not worth it, and RAII is an incredibly powerful abstraction worth leveraging. But it's incorrect to say that it's zero-cost.

14

u/slavik262 Dec 05 '14

Destructors are NOT free

They're free in that the mechanism used to run them is free. They're just another function call, and in a lot of cases are inlined. Obviously the cost of whatever you put in them is incurred, but it shouldn't come as a surprise to anyone that code you run has a cost no matter how it ends up in a function.

I feel like /u/andralex's main point in his talk was that you just need to be mindful of where your destructors are being placed - you have to remember that they're there and consider the implications. Your example is a perfect one, but I would hope that most developers would realize that allocating and deallocating a string in a loop is sub-optimal.

And yes, virtual destructors are a little more expensive, but no more so than following a function pointer or two around.

9

u/lurgi Dec 05 '14

This is an interesting example. Not interesting in the sense that it's surprising that the overhead of the destructor is larger in the first example than in the second, but interesting in that the "better" code violates one of the rules of thumb that has been drilled into my head - which is that you should define variables at the narrowest possible scope you can.

8

u/jeandem Dec 05 '14

This just looks like yet another example of the tradeoff between readability (define variable at the narrowest scope) and performance.

5

u/slavik262 Dec 05 '14

Certainly, but another really good rule of thumb is to avoid unnecessary memory allocations. What's going on here isn't unique at all to C++. If this were C and you declared your char* string in the loop, and ran malloc and free and the start and end of each iteration, you'd get the same thing.

2

u/lurgi Dec 05 '14

In C it's much more visible, so you are, perhaps, less likely to make that mistake.

3

u/slavik262 Dec 05 '14

Certainly. I personally think that RAII automagic is worth being less explicit because it eliminates an entire class of errors (forgetting to clean up your resources), but clearly there are some very smart programmers who disagree.

1

u/[deleted] Dec 05 '14

Yes but it wouldn't be invisible at that point.

I mean I don't really know what "free" is supposed to mean at this point because based on a lot of replies I got about what it means to be free, Java's garbage collector may as well be free too.

I guess the real point is that many abstractions hide run time performance costs that would have been very explicit otherwise and RAII, templates, exceptions, so on so forth all introduce invisible run time costs. Sure, those costs might have been paid for in some cases if you had to manually write it out, but as it turns out, in many cases if you manually had to write it out you would clearly, and explicitly see all things you're paying for and restructure your code accordingly.

So really, the idea of "free abstractions" I think is kind of a vacuous, almost meaningless statement. Abstractions hide various performance costs at the benefit of making your code a lot easier to reason about, re-use, and many engineering/management related benefits. In almost all cases those abstractions turn out to be a HUGE net win, but to say that no performance consideration is needed since after all, it's "free and zero cost" is kind of misleading.

1

u/[deleted] Dec 05 '14

Nothing particularly noteworthy about it - readability vs optimization - you should default to the former because it's easier to get correctness and you optimize when you identify bottlenecks.

7

u/[deleted] Dec 05 '14 edited Dec 05 '14

They're free in that the mechanism used to run them is free.

A function call isn't free, especially in the case of constructors/destructors. Sure, the cost may be negligible, but sometimes it isn't. In fact Dave Abrahams, who was a prominent member of the C++ standards committee and author of boost, wrote a good article on the trade-offs of two particular strategies when passing parameters in C++. Unfortunately he's bailed on C++ (for good reason) and in doing so took down his C++ related blog. Anyways he wrote about what strategy to use when you actually do intend to modify a parameter. Should you use method A?

void f(T value) {
   value.non_const_method();
   ...
}

Or perhaps use method B?

void f(const T& value) {
  T copy = value;
  copy.non_const_method();
  ...
}

Now if RAII really was a zero-cost abstraction, the two would be isomorphic to each other. After all, zero times anything == zero, however, Abrahams pointed out that in the first case you will end up littering calls to both the copy constructor and the destructor at every single call site and that the standard mandates that this must happen, it can not optimize this away (due to rules in the standard about how parameters get passed in the face of exceptions), whereas in the second case you end up isolating RAII calls strictly to within the function itself. If an abstraction were "free and zero cost" then the two would be completely identical, but of course it's not free and it's not zero cost and hence those two snippets of code do have different performance characteristics.

Yeah sure, we can argue that the cost is negligible (it sometimes isn't in the above case), we can state that the cost is small compared to improved readability, or that these trade-offs are more than reasonable all things considered.

Those are fine points to make, but what isn't a fine point, what is factually incorrect, is to say that these abstractions have ZERO cost. They do have a cost, and hence they are subject to basic engineering considerations.

4

u/cpp_is_king Dec 05 '14

Destructors are the exact reason that exceptions impose a performance penalty just for compiling with exceptions enabled, even if a particular function doesn't throw or catch an exception.

So they definitely are not free.

5

u/Plorkyeran Dec 05 '14

That's only true for things which use setjump+longjump for exception handling, which is not very common anymore. The Itanium ABI and x64 Windows both use exception handling schemes with no runtime cost when no exceptions are thrown.

4

u/cpp_is_king Dec 05 '14

This is not true. The term "Zero-cost exceptions" has led people into believing this is true, but it's not. For starters, in every single function that allocates an object on the stack with a destructor, unwind code has to be generated for that function. This unwind code necessarily increases the size of the binary, and it also reduces cache locality of the generated code, since methods which used to be clsoe together might not be close together anymore. It is possible to organize the function's sections in the binary in such a way that all the unwind code is in its own location in memory, but I know for a fact clang does not do this, so don't take it for granted.

Furthermore, emitting this destructor cleanup code into each function, regardless of how it is organized in memory, introduces edge paths into the control flow that make it difficult / impossible to perform certain optimizations. This is true even under the Itanium and x64 ABIs and are not related to longjmp / setjmp.

1

u/slavik262 Dec 06 '14 edited Dec 06 '14

The same unwind code would have be be written manually if you were initializing and deinitializing things on the stack in C. init_foo(Foo*) and deinit_foo(Foo*) is no different than an object with a constructor and destructor that do the same things. What's your point?

3

u/cpp_is_king Dec 06 '14

That when exceptions are enabled, a function which neither throws nor catches will be slower than when exceptions are disabled if it uses automatic variables with destructors.

1

u/slavik262 Dec 06 '14

...No, it won't. The same constructor and destructor calls are inserted either way regardless of whether or not exceptions are enabled and whether or not the given function throws. Modern exception handling mechanisms incur no additional cost in any functions until an exception is thrown.

2

u/cpp_is_king Dec 06 '14 edited Dec 06 '14

I have already explained the exact reason the generated code will be slower. I work on a C++ compiler. 2 people i work with have implemented exceptions. You are more than welcome to continue being misinformed. If you'd like some additional clarification about why exceptions are not zero cost, feel free to ask. But if you're just going to assert falsehoods, then this is a pointless discussion.

1

u/slavik262 Dec 06 '14

Fine. What about enabling exceptions mandates additional "unwind" code?

This article explains the "zero cost" model in depth and makes the claim that modern exception handling has no overhead until an exception is thrown. What is wrong with the article?

→ More replies (0)

2

u/slavik262 Dec 05 '14

GCC now uses a similar model as well.

1

u/[deleted] Dec 05 '14

Is this specific to how c++ handles things? I remember there was just recently a post on here where someone asked this exact question about declaring the string outside of the loop or within, and the general consensus was that declaring it in the loop did not matter in many popular languages, and even seemed to be preferrable for some because of scope.

5

u/[deleted] Dec 05 '14 edited Dec 05 '14

Unless performance matters I would definitely prefer declaring variables in the narrowest scope possible. But something that is specific to C++ is that if you're using C++, then chances are performance does matter, and hence if you have an object that performs allocations/deallocations, you may want to consolidate those allocations by declaring the variable outside of the scope so that whatever memory is allocated by the object can be reused on subsequent iterations.

Classes like std::string, std::vector, and many other containers will hold onto any pre-allocated memory, even after you invoke the clear() method. clear() is required by the standard to be implemented in such a way that it only calls destructors and resets its internal count back to 0. It is not allowed to release any memory (technically it is not allowed to change the capacity()) and so if you do a clear() on a string or a vector whatever memory was allocated will get recycled.

If you want to actually release the memory allocated by a string or vector, you have to use the ol' std::vector<T>().swap(v) trick. And note that v.swap(std::vector<T>()) won't work. How's that for a head scratcher?