Why garbage collection when RAII is available?
Asked Answered
M

7

43

I hear talks of C++14 introducing a garbage collector in the C++ standard library itself. What is the rationale behind this feature? Isn't this the reason that RAII exists in C++?

  • How will the presence of standard library garbage collector affect the RAII semantic?
  • How does it matter to me(the programmer) or the way in which I write C++ programs?
Mease answered 23/6, 2013 at 14:48 Comment(11)
I heard once Herb saying it would be very beneficial for some situations in lock-free programming, but since I don't know much about it, I won't try answeringPerisarc
@AndyProwl: Same here. Hopefully, someone who knows better will try answering.Mease
The lock-free thing is simple: many lock-free algorithms are super simple if you are allowed to leak the memory.Holdall
@R.MartinhoFernandes: Care to elaborate / give an example?Mayda
Also, Herb says not EVERYTHING should be garbage collected, only the stuff related to the lock-free thing. He also says the GC should be accurate.Wealthy
C++ is not a bird in a cage like objective-C or .NET, it is used everywhere from microcontrollers to servers, unpredictable utilities may introduce new concerns and full control over memory is what makes C++ preferrable than managed languages.Boondocks
AFAIK the standard only cares about GC enough to make implementing one possible, so it can be used as an optional way to manage some of the memory. GC is not going to replace RAII or the scope bound object lifetime it is based on.Hakodate
@Mayda Take removing an element from a linked list as an example. Removing it is very simple, a simple cas and you're done... Well not quite; when is it safe to delete the node? Any number of other threads may currently hold a reference to that node and it's very difficult to determine when it's safe to delete it. GC takes care of that problem easily by having another thread do all the reference counting for you in a thread safe manner. Personally, I much prefer RCU which is far less invasive and has had great deal of success in the Linux kernel.Eckhart
Where have you heard anything about C++14 getting GC? Because it certainly isn't in the current working draft.Congruency
@Ze, can std::shared_ptr not solve the given problem elegantly? e.g. use std::shared_ptr<Node> instead of Node* as the type of field next.Debra
@Debra shared_ptr are not enough. Imagine a thread that reads the contents of the shared_ptr and is immediately pre-empted by the thread deleting the node. When it resumes execution, it has no way of determining that the contents it read is still valid because it never incremented the refcount and it was perfectly legal for the deleting thread to free the node. It's a thorny problem but it can be solved by EBR, RCU, hazard pointers, full featured GC or context-sensitive solutions.Eckhart
W
33

Garbage collection and RAII are useful in different contexts. The presence of GC should not affect your use of RAII. Since RAII is well-known, I give two examples where GC is handy.


Garbage collection would be a great help in implementing lock-free data structures.

[...] it turns out that deterministic memory freeing is quite a fundamental problem in lock-free data structures. (from Lock-Free Data Structures By Andrei Alexandrescu)

Basically the problem is that you have to make sure you are not deallocating the memory while a thread is reading it. That's where GC becomes handy: It can look at the threads and only do the deallocation when it is safe. Please read the article for details.

Just to be clear here: it doesn't mean that the WHOLE WORLD should be garbage collected as in Java; only the relevant data should be garbage collected accurately.


In one of his presentations, Bjarne Stroustrup also gave a good, valid example where GC becomes handy. Imagine an application written in C/C++, 10M SLOC in size. The application works reasonably well (fairly bug free) but it leaks. You neither have the resources (man hours) nor the functional knowledge to fix this. The source code is a somewhat messy legacy code. What do you do? I agree that it is perhaps the easiest and cheapest way to sweep the problem under the rug with GC.


As it has been pointed out by sasha.sochka, the garbage collector will be optional.

My personal concern is that people would start using GC like it is used in Java and would write sloppy code and garbage collect everything. (I have the impression that shared_ptr has already become the default 'go to' even in cases where unique_ptr or, hell, stack allocation would do it.)

Wealthy answered 24/6, 2013 at 8:43 Comment(7)
Garbage collection can also come handy when you have to use the heap but freeing objects happens very frequently. Garbage collection is allowed to postpone freeing resources until memory gets scarce and then collect all memory at once. if you program has not been optimized to use memory pools or free lists but suffers from this problem it might benefit from this fact.Tricuspid
@Alex: there are other ways to handle such a thing, like custom allocators/deallocators and smart-pointerWarrington
@akappa: why do memory pooling manually if you could have a scoped garbage collector doing the same things with equivalent performance?Tricuspid
@Alex: I prefer having library-based, non-invasive and explicit solutions to something like "embedding a GC into the C++ runtime", which do sound very invasive and subtle. But that's just my preference, so I can understand why people do things differently.Warrington
@akappa: when GC will be added to C++ it will hopefully still be c++. Meaning: everything that costs performance will be opt-in. (you don't have to use it). And having a GC when performance would be better than smart-pointers is also nice to have. I've written a smart pointer + pool + custom deallocator to postpone deallocation and reduce heap pressure, just having that as a language feature instead of hand written code is very nice.Tricuspid
somebody should tell bjarne about valgrind :PSovran
@Sovran valgrind wouldn't help much. If the code is messy and you don't have functional knowledge, it can be quite tricky to fix the leak. Unfortunately, many companies have this problem (I mean messy legacy code and very few developers who can and are willing to work on that code).Wealthy
S
11

I agree with @DeadMG that there is no GC in current C++ standard but I would like to add the following citation from B. Stroustrup:

When (not if) automatic garbage collection becomes part of C++, it will be optional

So Bjarne is sure that it will be added in future. At least the chairman of the EWG (Evolution Working Group) and one of the most important committee members (and more importantly language creator) wants to add it.

Unless he changed his opinion we can expect it to be added and implemented in the future.

Shackleton answered 23/6, 2013 at 15:53 Comment(3)
Actually, the "head" (or, more precisely, the convener) of the C++ committee is Herb Sutter. Bjarne Stroustrup chairs the Evolution Working Group (EWG).Hellhole
@sasha.sochka: What do you think of my answer? I'm not sure if it agrees with anything people in the EWG are actually thinking, but it would seem a very important benefit of having a language recognize GC, rather than trying to implement GC via a library the language itself knows nothing about.Librium
@supercat, I think your answer is reasonable. I don't know anything about plans of the working group but the fact that Stroustrup promises to make GC optional makes it very realistic for GC to be implemented in the language itself. Old code will not use GC and new code will chose the mode, using some compiler flag, for example. +1Shackleton
M
11

There are some algorithms which are complicated/inefficient/impossible to write without a GC. I suspect this is the major selling point for GC in C++, and can't ever see it being used as a general-purpose allocator.

Why not a general-purpose allocator?

First, We have RAII, and most (including me) seem to believe that this is a superior method of resource management. We like determinism because it makes writing robust, leak-free code a lot simpler and makes performance predictable.

Second, you'll need to place some very un-C++-like restrictions on how you can use memory. For instance, you'd need at least one reachable, un-obfuscated pointer. Obfuscated pointers, as are popular in common tree container libraries (using alignment-guaranteed low bits for color flags) among others, won't be recognizable by the GC.

Related to that, the things which make modern GCs so usable are going to be very difficult to apply to C++ if you support any number of obfuscated pointers. Generational defragmenting GCs are really cool, because allocating is extremely cheap (essentially just incrementing a pointer) and eventually your allocations get compacted into something smaller with improved locality. To do this, objects need to be movable.

To make an object safely movable, the GC needs to be able to update all the pointers to it. It won't be able to find obfuscated ones. This could be accomodated, but wouldn't be pretty (probably a gc_pin type or similar, used like current std::lock_guard, which is used whenever you need a raw pointer). Usability would be out the door.

Without making things movable, a GC would be significantly slower and less scalable than what you're used to elsewhere.

Usability reasons (resource management) and efficiency reasons (fast, movable allocations) out of the way, what else is GC good for? Certainly not general-purpose. Enter lock-free algorithms.

Why lock-free?

Lock-free algorithms work by letting an operation under contention go temporarily "out of sync" with the data structure and detecting/correcting this at a later step. One effect of this is that under contention memory might be accessed after it has been deleted. For example, if you have multiple threads competing to pop a node from a LIFO, it is possible for one thread to pop and delete the node before another thread has realized the node was already taken:

Thread A:

  • Get pointer to root node.
  • Get pointer to next node from root node.
  • Suspend

Thread B:

  • Get pointer to root node.
  • Suspend

Thread A:

  • Pop node. (replace root node pointer with next node pointer, if root node pointer hasn't changed since it was read.)
  • Delete node.
  • Suspend

Thread B:

  • Get pointer to next node from our pointer of root node, which is now "out of sync" and was just deleted so instead we crash.

With GC you can avoid the possibility of reading from uncommitted memory because the node would never be deleted while Thread B is referencing it. There are ways around this, such as hazard pointers or catching SEH exceptions on Windows, but these can hurt performance significantly. GC tends to be the most optimal solution here.

Monogamist answered 23/6, 2013 at 17:20 Comment(8)
Where does this explanation ties specifically to lock-free algorithms? This just looks like a basic race condition that can be solved without obligatory resorting to GC. You say with a GC it would not crash, because the node would never be deleted while Thread B references it (Thread B just get root node in step 2), so, would it be OK to read what has been pop'ed but not deleted (step 4)?Setose
Yes, a lock-free LIFO can read a popped node. This is expected behavior. With GC you'd not delete, so there'd be no access to possibly uncommitted memory.Monogamist
I suspect you didn't get my point, I know it won't crash and hence it's OK in that sense, what I'm asking is whether it's OK to have a popped node at hand for use, if it's OK, it should be demonstrated why it's OK for Thread B to use such already popped node and in which way it would be used.Setose
I believe I understood, but didn't explain well enough. Sorry. The lock-free LIFO pop() goes like this: read head ptr from data structure; read next ptr from head ptr (which at this point may not be the true head node anymore); atomically replace head ptr in data structure with next ptr, if head ptr hasn't changed. There are no locks during this, so under contention it is possible for Thread A to complete a full pop() right in the middle of Thread B's pop(). So it is expected that a thread will read stale data under contention, and the final step being atomic corrects for that.Monogamist
Such data structures may work better with GC than RAII, but they seem obscure. Strings are very common, and can also work better with GC than RAII (though they could work better yet in a language that supported both). Every time a persistable string reference is created or destroyed in a non-GC system, it's generally necessary to use atomic operations to ascertain whether that reference is the last one. Atomic operations are expensive on multi-processor systems. In a GC system, persisting a string reference requires nothing more than copying the reference.Librium
While it's possible to avoid the complexities of persisting string reference by having code copy the content of any string it wants to persist, that can add substantial runtime cost. Not only does it increase memory consumption and increase the time spent copying strings--it also prevents fast detection of strings that are reference-equal. RAII support could be used to distinguish strings which cannot possibly have more than one reference from those which "might", thus allowing in-place modification of strings for which only one reference could possibly exist, but GC is good for strings.Librium
Anything allocation-heavy (like creating lots of strings) will typically be faster in a defragging GC due to allocation usually being a mere increment to a pointer. I don't think C++ can support moving objects like this, though, so this perf benefit might not exist for us.Monogamist
Also, GC can be faster by default, but there's almost always going to be an optimization you can do to improve perf where it matters without a GC. Pools, arenas, etc. -- not everything needs to use the generalized standard allocator. This practice is very common in embedded code and games.Monogamist
C
6

There isn't, because there isn't one. The only features C++ ever had for GC were introduced in C++11 and they're just marking memory, there's no collector required. Nor will there be in C++14.

There is no way in hell a collector could pass Committee, is my opinion.

Clough answered 23/6, 2013 at 15:1 Comment(3)
+1; also the C++14 committee draft doesn't introduce garbage collection: isocpp.org/files/papers/N3690.pdfDisappoint
This doesn't answer the question. You don't even explain why you have such a strong opinion.Hexaemeron
@anatolyg: The question asks what the consequences are of garbage collection in C++14. There are none, because there is none. That is the answer to his question. My opinion is just a note, really.Clough
W
4

GC has the following advantages:

  1. It can handle circular references without programmer assistance (with RAII-style, you have to use weak_ptr to break circles). So a RAII style application can still "leak" if it is used improperly.
  2. Creating/destroying tons of shared_ptr's to a given object can be expensive because refcount increment/decrement are atomic operations. In multi-threaded applications the memory locations which contains refcounts will be "hot" places, putting a lot of pressure on the memory subsystem. GC isn't prone to this specific issue, because it uses reachable sets instead of refcounts.

I am not saying that GC is the best/good choice. I am just saying that it has different characteristics. In some scenarios that might be an advantage.

Wellrounded answered 24/6, 2013 at 10:38 Comment(4)
Note that even with a GC you can "leak". Imagine a event handler where you register delegates but forget to unregister those delegates when you're done with the object. From the moment you forgot to unregister you leak the object as it will never be collected by the GC as there is still a valid reference to it. Now imagine the delegate holds a reference to some big object -> bang!Totality
@Totality Yes, it is still possible to have logical leaks, though those are much rarer. Reference cycles alone don't cause that. In your example, the leak only occurs if the delegate is strongly reachable (likely through the list of event handlers). That could happen if the event is global/static, but that's not the case in many many cases. Also note that this is not a leak in the sense that it can't possibly be accessed any more: As the event is reachable, it could be invoked later on. If it is, you got a bug anyway. Only if it isn't you actually have a leak, but that's even rarer.Ep
A RAII application would not have "tons of shared_ptr's"Kirstenkirsteni
A much bigger advantage of compiler-supported garbage collection is that as long as a reference to a dead object exists, it is guaranteed to always refer to that same dead object. By contrast, when using RAII, a reference (or pointer) to a dead object may spontaneously become a seemingly-valid reference to some arbitrary live object. Memory leaks are bad, but dangling references are worse; RAII can't very well prevent dangling references from invoking Undefined Behavior, but compiler-supported garbage collection can.Librium
L
4

None of the answers so far touch upon the most important benefit of adding garbage-collection to a language: In the absence of language-supported garbage-collection, it's almost impossible to guarantee that no object will be destroyed while references to it exist. Worse, if such a thing does happen, it's almost impossible to guarantee that a later attempt to use the reference won't end up manipulating some other random object.

Although there are many kinds of objects whose lifetimes can be much better managed by RAII than by a garbage collector, there's considerable value in having the GC manage nearly all objects, including those whose lifetime is controlled by RAII. An object's destructor should kill the object and make it useless, but leave the corpse behind for the GC. Any reference to the object will thus become a reference to the corpse, and will remain one until it (the reference) ceases to exist entirely. Only when all references to the corpse have ceased to exist will the corpse itself do so.

While there are ways of implementing garbage collectors without inherent language support, such implementations either require that the GC be informed any time references are created or destroyed (adding considerable hassle and overhead), or run the risk that a reference the GC doesn't know about might exist to an object which is otherwise unreferenced. Compiler support for GC eliminates both those problems.

Librium answered 23/7, 2013 at 20:27 Comment(3)
If something tries to use a reference to a destructed object, then you have a bug in your program. Using GC to hide the problem won't help you in the long run.Pita
@AndrewDurward: In cases where GC objects can accept notification that they are no longer needed, they can be coded to explicitly trap on attempts to use them after that. GC wouldn't hide the problem--quite the opposite. Instead of having a program behave randomly, it would deterministically trap at the error. Further, many scenarios that could cause errant destruction in C++ would be non-issues in .NET or Java. For example, in GC frameworks, one may safely pass references to immutable data structures between threads, and the threads may use those references as they see fit...Librium
...without needing any sort of memory synchronization. In C++, if multiple threads may be creating and destroying references to an immutable object, then unless all code which may increase or decrease the number of extant references uses memory synchronization when updating any reference counts, it may be hard to avoid such counts getting out of sync.Librium
D
1

Definitions:

RCB GC: Reference-Counting Based GC.

MSB GC: Mark-Sweep Based GC.

Quick Answer:

MSB GC should be added into the C++ standard, because it is more handy than RCB GC in certain cases.

Two illustrative examples:

Consider a global buffer whose initial size is small, and any thread can dynamically enlarge its size and keep the old contents accessible for other threads.

Implementation 1 (MSB GC Version):

int*   g_buf = 0;
size_t g_current_buf_size = 1024;

void InitializeGlobalBuffer()
{
    g_buf = gcnew int[g_current_buf_size];
}

int GetValueFromGlobalBuffer(size_t index)
{
    return g_buf[index];
}

void EnlargeGlobalBufferSize(size_t new_size)
{
    if (new_size > g_current_buf_size)
    {
        auto tmp_buf = gcnew int[new_size];
        memcpy(tmp_buf, g_buf, g_current_buf_size * sizeof(int));       
        std::swap(tmp_buf, g_buf); 
    }   
}

Implementation 2 (RCB GC Version):

std::shared_ptr<int> g_buf;
size_t g_current_buf_size = 1024;

std::shared_ptr<int> NewBuffer(size_t size)
{
    return std::shared_ptr<int>(new int[size], []( int *p ) { delete[] p; });
}

void InitializeGlobalBuffer()
{
    g_buf = NewBuffer(g_current_buf_size);
}

int GetValueFromGlobalBuffer(size_t index)
{
    return g_buf[index];
}

void EnlargeGlobalBufferSize(size_t new_size)
{
    if (new_size > g_current_buf_size)
    {
        auto tmp_buf = NewBuffer(new_size);
        memcpy(tmp_buf, g_buf, g_current_buf_size * sizeof(int));       
        std::swap(tmp_buf, g_buf); 

        //
        // Now tmp_buf owns the old g_buf, when tmp_buf is destructed,
        // the old g_buf will also be deleted. 
        //      
    }   
}

PLEASE NOTE:

After calling std::swap(tmp_buf, g_buf);, tmp_buf owns the old g_buf. When tmp_buf is destructed, the old g_buf will also be deleted.

If another thread is calling GetValueFromGlobalBuffer(index); to fetch the value from the old g_buf, then A Race Hazard Will Occur!!!

So, though implementation 2 looks as elegant as implementation 1, it doesn't work!

If we want to make implementation 2 work correctly, we must add some kind of lock-mechanism; then it will be not only slower, but less elegant than implementaion 1.

Conclusion:

It is good to take MSB GC into the C++ standard as an optional feature.

Debra answered 15/9, 2013 at 6:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.