Skip to main content
GameDev.net gamedev.net
🔒 Locked

What garbage collection / memory management do you use?

Started by Chris81 May 13, 2005 at 1:23 AM 19 replies 3.3k views
Original Post
Chris81
Chris81
Smart Pointers? (If so, what kind?) Handle Based? (If so, how do you deal with pointers?) Combination? Nothing at all? What aboug Sun's libgc...would that be fast enough for a 3D engine? Too bad search is disabled because I'm sure there are plenty of forum posts on this already. Thanks though.
dave
dave
I use auto_ptr wherever i need a pointer to something that doesn't need to go into a container and shared_ptr from boost if it does.

ace
Nitage
Nitage
I use boost::shared_ptr, boost::weak_ptr and boost::scoped_ptr.

I also use factories with memory pools for some objects.
Fruny
Fruny
Reference-counting smart pointers. Typically boost::shared_ptr, or Loki::SmartPtr.

I still want to try the Boehm GC one day...
"Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it." — Brian W. Kernighan
Trap
Trap
boost::intrusive_ptr can be a lot faster than boost::shared_ptr, up to a 10 times difference in extreme cases (reversing a reference counted linked list).
Feral
Feral
I use a normal ol boring *, an them I’m careful with it. By and large I don’t think it bites me too often.
Extrarius
Extrarius
I'm working on a smart-pointer system with both strong and weak pointes that does both reference counting and mark-sweep garbage collection. The idea is that reference counting will work for 95% of everything, and the mark-sweep will clean up cycles and such as long as the strong and weak pointers are used correctly. If not, then the final pass of the memory manager will still free everything but it might not be pretty.
Oh, and I'm also going to have a long-term allocator that is collected in it's entirety at certain points (for things like the level's data that won't be needed after level change but doesn't need to be collected while playing that level).
"Walk not the trodden path, for it has borne it's burden." -John, Flying Monk
antareus
antareus
A few little classes that I wrote.

AutoPtr is for resources that are later 'handed off' to another lifetime management system. (This is std::auto_ptr.)

ScopedPtr is for resources that are *only* needed within that scope (it is AutoPtr, but with no way to release the resource.)

ReferenceLink is similar to a reference counting, but doesn't require any free store allocation. It does, however, require two pointers per object. Very rarely needed.
--God has paid us the intolerable compliment of loving us, in the deepest, most tragic, most inexorable sense.- C.S. Lewis
Telastyn
Telastyn
For some things, a reference counted pointer [to be replaced with boost or similar]

The rest of my pointers/dynamic objects are stored in a home made container class which does auto-deletion depending on the type stored.

Mainly though, I've found that the better I get with design, the less need I have for pointers strewn across the app which need tracking.
ToohrVyk
ToohrVyk
I try to use garbage collected languages as often as possible (C#, Java, OCaml), and when using C++ I use either boost::shared_ptr or static allocation on the heap.
snk_kid
snk_kid
Quote:
Original post by Trap
boost::intrusive_ptr can be a lot faster than boost::shared_ptr, up to a 10 times difference in extreme cases (reversing a reference counted linked list).


Thats all well and good but you left an important factor out and its hinted in the name of intrusive_ptr, "intrusive" exactly that and only works with user-defined types that maintain an embedded reference count and define two particular free-functions intrusive_ptr_add_ref & intrusive_ptr_release.
Chris81
Chris81
So it seems most people are using smart pointers. Here is what I'm thinking of using:

A handle-based system where the handles have a lifetime. So, for example you create an object by using the manager which returns a handle. You specify a lifetime, such as; frame, level, game, or custom. It automatically "returns" the handle to the object pool at the proper time it is marked for, which can then be reused. The only time you can get a pointer to the object is if you use the "custom" lifetime which means you have to explicitly return the object when you're ready. It is then easy for the manager to track these pointers and give warnings/errors. (The manager in turn uses a size-sorted btree for the heap to get O(n log n) average)

Granted, this still requires the programmer to keep track of releasing objects, however it is very quick to catch leaks - which is the main reason for doing all of this. This also assumes you use the manager class for everything, but that's true for all c/c++ GC's.

But it seems very few engine authors use a handle system, and opt for smart pointers. Why is that, when handles are much simpler, cleaner, and help with serialization?
Kylotan
Kylotan
Usually I just use a bare pointer and am careful to keep track of it. I occasionally use a boost smart pointer when I have a resource with a lifetime that corresponds to that of another object or a function's scope. Equally occasionally, I'll just allocate a vector of objects and pass out the indices as handles.

Personally I don't find memory management to be a large enough problem that I feel the need to use many added tricks to manage it. Generally I like to know exactly when my objects start and end their lifetimes, and the new/delete calls just go hand in hand with that.

Having said that, I was quite familiar with the concepts of indexed addressing, indirect addressing, and offset addressing long before I set eyes on C or C++, so low level pointer trickery is all natural to me. I can certainly see the benefit of garbage collection and reference counting for those who aren't as familiar with the low level details.
chollida1
chollida1
Those are good points Kylotan. where lifetime management wrappers come in handy is with exceptions. Since they provide stack based cleanup attriubtes to dynamically allcoated resources you never have to worry about added cleanup code.


no more
if( failed )
delete ptr;

if( failed)
delete ptr;
delte newptr;

etc.


Cheers
Chris
CheersChris
Andrew Russell
Andrew Russell
Quote:
Original post by chollida1
Those are good points Kylotan. where lifetime management wrappers come in handy is with exceptions. Since they provide stack based cleanup attriubtes to dynamically allcoated resources you never have to worry about added cleanup code.

Which is why you use a smart pointer (a scoped pointer or some kind of reference-counted pointer, etc) and use exception handling (try/throw/catch).

The reason for this is - when you throw, all your local variables are cleaned up for you. So your local smart pointer gets destroyed, but also its deconstructor runs - meaning that the resouce is deleted (if not still in use - in the case of a reference-counted pointer).

I personally use my own implementation of a smart pointer that works like boost::shared_ptr, because boost::shared_ptr really hates intellisense and it got far too annoying.

I don't think I'd ever use something more complicated for normal C++. The only time I would use a GC would be if I were making it a feature of a scripting language. Even then, reference counted pointers may be sufficient - although I did help work on a scripting language once, and alocations and dealocations using shared pointers were very slow (particilarly because every variable was new'ed and deleted).
chollida1
chollida1
Didn't I just say that:)

cheers
CHris
CheersChris
Jan Wassenberg
Jan Wassenberg
- Resources are accessed via handle. A central manager takes care of refcounting, type safety, lifetime, protection vs. double free, caching, leak detection, debug information (whodunit) etc. No pointers are needed at all.

What Chris81 describes works well in practice :)
Quote:
But it seems very few engine authors use a handle system, and opt for smart pointers. Why is that, when handles are much simpler, cleaner, and help with serialization?

Probably due to exceptions. For insight into why that is a bad means of error *handling* (!), see the ongoing "[Sweng-gamedev] Effective use of C++ exceptions" thread and Raymond Chen's blog (1, 2).
This is why I strongly prefer interfaces be function-based with error codes; exceptions are allowed but are considered fatal if they escape out of a module.

- Lock-free data structures need some kind of (fast, incremental) garbage collection. In Linux it's RCU; lacking such kernel support, what I do is keep per-thread freelists with hazard pointers.
E8 17 00 42 CE DC D2 DC E4 EA C4 40 CA DA C2 D8 CC 40 CA D0 E8 40E0 CA CA 96 5B B0 16 50 D7 D4 02 B2 02 86 E2 CD 21 58 48 79 F2 C3
MaulingMonkey
MaulingMonkey
Quote:
Original post by Jan Wassenberg
Probably due to exceptions. For insight into why that is a bad means of error *handling* (!), see the ongoing "[Sweng-gamedev] Effective use of C++ exceptions" thread and Raymond Chen's blog (1, 2).
This is why I strongly prefer interfaces be function-based with error codes; exceptions are allowed but are considered fatal if they escape out of a module.

- Lock-free data structures need some kind of (fast, incremental) garbage collection. In Linux it's RCU; lacking such kernel support, what I do is keep per-thread freelists with hazard pointers.


Surely, I'm misunderstanding you when I see what appears to be you arguing against exceptions based on those two blog articles, which would appear to be some of the piss-poor-est arguments against exceptions I've ever seen. He takes this "imaginary" code:

BOOL ComputeChecksum(LPCTSTR pszFile, DWORD* pdwResult){  HANDLE h = CreateFile(pszFile, GENERIC_READ, FILE_SHARE_READ,       NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);  HANDLE hfm = CreateFileMapping(h, NULL, PAGE_READ, 0, 0, NULL);  void *pv = MapViewOfFile(hfm, FILE_MAP_READ, 0, 0, 0);  DWORD dwHeaderSum;  CheckSumMappedFile(pvBase, GetFileSize(h, NULL),           &dwHeaderSum, pdwResult);  UnmapViewOfFile(pv);  CloseHandle(hfm);  CloseHandle(h);  return TRUE;}


And points out it's obvious poor quality due to the complete lack of error checking. Switching this to using RAII and exceptions would look something like this:

BOOL ComputeChecksum( LPCTSTR pszFile , DWORD * pdwResult ){    file h = CreateFile( pszFile , .... );    file hfm = CreateFileMapping( h , .... );    mapping pv = MapViewOfFile( hfm , .... );    DWORD dwHeaderSum;    CheckSumMappedFile( pv , .... );    return true;}


Instead of:

BOOL ComputeChecksum(LPCTSTR pszFile, DWORD* pdwResult){  BOOL fRc = FALSE;  HANDLE h = CreateFile(pszFile, GENERIC_READ, FILE_SHARE_READ,       NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);  if (h != INVALID_HANDLE_VALUE) {    HANDLE hfm = CreateFileMapping(h, NULL, PAGE_READ, 0, 0, NULL);    if (hfm) {      void *pv = MapViewOfFile(hfm, FILE_MAP_READ, 0, 0, 0);      if (pv) {        DWORD dwHeaderSum;        if (CheckSumMappedFile(pvBase, GetFileSize(h, NULL),                               &dwHeaderSum, pdwResult)) {          fRc = TRUE;        }        UnmapViewOfFile(pv);      }      CloseHandle(hfm);    }    CloseHandle(h);  }  return fRc;}


The results would be the same... on success, the function would return true. On a failure, the function would clean up the resources and report the error (by returning false in the spaghetti version, and by throwing an exception in the good version).

Then, he goes on to argue against exceptions due to the possibility of writing this kind of code:

NotifyIcon CreateNotifyIcon(){ NotifyIcon icon = new NotifyIcon(); icon.Text = "Blah blah blah"; icon.Visible = true; icon.Icon = new Icon(GetType(), "cool.ico"); return icon;}


In which icon.Icon is set after icon.Visible, which apparently will cause an exception. I can cause an error with piss-poor spaghetti code just as easily (ignoring the fact that I can't return an error code and an icon, another problem of error-code returns):

NotifyIcon CreateNotifyIcon(){    bool result = false;    NotifyIcon icon = new NotifyIcon();    if ( icon.SetText( "Blah blah blah" ) )    {        if ( icon.SetVisible( true ) )        {            if ( icon.SetIcon( GetType() , "cool.ico" ) )            {                result = true;            }            //no cleanup for this step even without RAII...        }        //no cleanup for this step even without RAII...    }    //no cleanup for this step even without RAII...    if ( ! result ) return false;    else return icon;}


I have yet to look at the other thread mentioned. I laughed when I read this:

"It's really hard to write good exception-based code since you have to check every single line of code (indeed, every sub-expression) and think about what exceptions it might raise and how your code will react to it. (In C++ it's not quite so bad because C++ exceptions are raised only at specific points during execution. In C#, exceptions can be raised at any time.)"

The entire point of exceptions is that you don't have to check every single line of code's return, and deal with it by hand, as you would without. I can write a long line of code, knowing that if an error occurs, it will be accurately described when it's finally handled, thanks to the fact that the exception contains such information (whereas error codes have a tendancy to loose precision, only telling if they in and of themselves succeeded, rather than what failed if anything 20 functions deep). Further, I won't have to worry about a huge mess of if statements just to make sure my resources get cleaned up because I'll have used RAII.

It's very easy to write good exception-based code once you know how to. I'm half convinced that article is a hoax.




Back on topic:

Containers and references, for the most part. Occasionally pointers, in which case I usually mantain container-like ownership (really, they're just the implementation details of a specialized container).
Jan Wassenberg
Jan Wassenberg
Frankly I have little interest debating the use of exceptions, especially when you only partially read what is presented and then scoff at it without understanding. But hey, let's give it a shot.

Quote:
Switching this to using RAII and exceptions would look something like this:

Since you clearly haven't read the comments, let this reply address what you proposed:
In your C++ version you're conveniently glossing over thecomplexity of, for example, making sure the file map is destroyed afterthe view (because the view relies on the file map). Are you going to use reference counting?

Actually I'm glad you brought this up, because it illustrates perfectly the point being made: it is much easier to fall flat on your ass with exception cleverness, as opposed to error codes (which may be bulky but get the job done and *are in plain sight*).

Quote:
(ignoring the fact that I can't return an error code and an icon, another problem of error-code returns):

Easy solution: either multiplex them (e.g. if 0 is returned, call GetLastError) or return data in an OUT parameter.

Quote:
I laughed when I read this: "It's really hard to write good exception-based code since you have to check every single line of code (indeed, every sub-expression) and think about what exceptions it might raise and how your code will react to it. (In C++ it's not quite so bad because C++ exceptions are raised only at specific points during execution. In C#, exceptions can be raised at any time.)"
The entire point of exceptions is that you don't have to check every single line of code's return, and deal with it by hand, as you would without.

If you don't know of Raymond Chen, that says more about you than him. If you do and are still laughing, then you probably haven't understood what's been said. So let's have another look: with return codes, you have to make sure each function call is wrapped in a CHECK_ERR macro (discussed by Alexandrescu under another name) or similar. Compare this with exceptions, where analysis is practically impossible because even trivial code sequences have an astounding number of paths through them. In particular, construction of [temp] variables may fail, throw, and leave your calculation in limbo. So instead of just looking for function calls and seeing that there's error handling code (direct or via CHECK_ERR), you have to think through every part of your code. Clearly RAII alone isn't enough to save you, as shown above.
And when you revisit code, who knows if anyone even gave a thought to error handling? After all, in exception-based code, that need not be apparent.
Now where does that leave us? Exceptions are surely good in some contexts (e.g. checking if object construction went OK), but this is much more limited than their actual use (c.f. the disgrace that is Xerces' end_of_entity "exception").

I like the exception use guide presented in one of the last mails in the current thread:
Raising an exception should be the equivalent of: 0) I have encountered an unexpected and exceptional situation1) This API has provided the caller with every means possible to pre-emptthis exception if it could have been expected and avoided2) There is no failsafe action I can take3) I have no means of indicating failure4) That the current situation has occurred is good cause to lose confidencein system integrity and/or safe continued exection of the current program.To continue would be worse than to abort.5) I will signal that the program should abort


Quote:
It's very easy to write good exception-based code once you know how to.

Looks like that's only true if using the Ostrich algorithm: stick_head_in_sand(). Unfortunately it really isn't that simple, or there wouldn't be entire books on the topic ;p

[Edited by - Jan Wassenberg on May 14, 2005 10:43:33 AM]
E8 17 00 42 CE DC D2 DC E4 EA C4 40 CA DA C2 D8 CC 40 CA D0 E8 40E0 CA CA 96 5B B0 16 50 D7 D4 02 B2 02 86 E2 CD 21 58 48 79 F2 C3

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.