Memory management dilemma

Daniel Keep daniel.keep.lists at gmail.com
Mon May 29 07:45:39 PDT 2006



Jeff Parsons wrote:
> Daniel Keep wrote:
> 
>>
>> Hi.  My computer just blue-screened while replying to this, so I hope
>> you appreciate me typing all this out again :P
>>
> 
> I certainly do; this has been really damn helpful. :)
> 
>>
>> If you want to pass stuff by-value, you can use a struct.
>>
>> Catch: no constructors or destructors.  But, you can do this instead:
>>
>> # struct Vector3D
>> # {
>> #     double x, y, z;
>> #
>> #     static Vector3D opCall(double x, double y, double z)
>> #     {
>> #         Vector3D result;
>> #         result.x = x; result.y = y; result.z = y;
>> #         return result;
>> #     }
>> # }
>> #
>> # Vector3D a = Vector3D(1,2,3);
>>
> 
> You've won me over. This certainly solves the "millions of temporaries
> being created" issue, for one!
> 
>>
>> Three main ways, actually:
>>
>> 1. Manually 'deleting' stuff: no fun at all.
> 
> Would it still be useful to do this sometimes in deconstructors if I
> know a few members that can safely be cleaned up immediately, so the
> garbage collector doesn't have to sift through things later? Or isn't
> this likely to give me any advantage at all?
> 

It gives you an advantage if you *want* them cleaned up in a timely
manner.  If you just don't care, then you can leave them for the GC.

There's a big catch to this, though: the GC will destroy objects in any
old fashion; it's non-deterministic.  The problem with this is:

# class Foo
# {
#     private Bar qux; // Bar is a class as well
#
#     // ...
#
#     ~this()
#     {
#         delete qux;
#     }
# }

That delete may fail!  Because you can't tell what order the GC cleans
stuff up, it's entirely possible that by the time Foo's destructor is
called, 'qux' may have already been destroyed.

HOWEVER, if you manually delete an object, or use 'auto', then you know
the exact order in which objects are destroyed, and there's no problem.

So the rule of thumb is: if a class is designed to be managed by the GC,
don't try to do anything to member objects in the destructor (doing
things like, say, freeing GL textures is fine since the GC doesn't
manage those).  On the other hand, if you always 'auto' or manually
delete instances of a class, manually deleting member objects is fine.

On a side note, unless I'm mistaken, the Ares project (which is an
alternate standard library to Phobos) has support for passing an arg
into destructors to indicate if they've been collected by the GC or
explicitly deleted.

>> 2. Using 'auto'.  Any object variable marked with 'auto' will
>> automatically destroy itself at the end of it's scope.  Example:
>>
>> # {
>> #     // ...
>> #
>> #     auto Foo bar = new Foo;
>> #
>> #     // ...
>> #
>> #     {
>> #
>> #         auto Foo baz = new Foo;
>> #
>> #     } // baz is destroyed here
>> #
>> #     // ...
>> #
>> # } // bar is destroyed here
>>
>> One thing to watch out for:
>>
>> Auto-deleted variable:
>> #    auto Foo bar = new Foo;
>>
>> Not auto-deleted, with type inference:
>> #    auto bar = new Foo;
>>
>> 'auto' without a type name after it means that D will work out the type
>> of the variable itself.  Just watch out for it :)
>>
>> If you want to make sure instances of a particular class are always
>> 'auto', you can put the 'auto' keyword out the front of the class
>> declaration:
>>
>> # auto class Foo
>> # {
>> #     // ...
>> # }
>> #
>> # // MUST be declared auto:
>> # auto Foo a;
>> # Foo a; // <-- this won't compile
>>
>> However, keep in mind that 'auto' objects can't be put into 'out' or
>> 'inout' function arguments, you can't have global 'auto' variables, you
>> can't have 'auto' members, and you can't return them out of functions
>> [1].
>>
>> 3. Using 'scope(exit)'.  You use 'scope(exit)' like a control statement:
>> any statement (or block of statements) you put after a 'scope(exit)'
>> will be executed when control leaves that scope.  For example:
>>
>> # {
>> #
>> #     // ...
>> #
>> #     Foo bar = new Foo;
>> #     scope(exit) delete bar;
>> #
>> #     // ...
>> #
>> # } // 'delete bar' is executed here
>>
>> Please note that 'scope(exit)' blocks will execute irrespective of HOW
>> you leave the scope: a return statement, break statement, goto or an
>> exception.
>>
>> There's also 'scope(fail)' which only executes if you leave the scope
>> via an exception, and 'scope(success)' for when you do NOT leave because
>> of an exception :)
> 
> Noted and understood; I'll certainly be making much use of -all- of
> that! :)
> 
>> As for dealing with temporaries (which I know I haven't actually
>> answered :P), then... we'll I don't know.  Unless you keep a reference
>> to each temporary object in an expression, I suppose you'd have to leave
>> the GC to handle that.
>>
>> That or, again, use structs.
> 
> Yeah, I think I'd be going for structs there. For one, I don't see it
> being an issue expect in cases like vectors and matrices where they're
> likely to be involved in a lot of number crunching.
> 
>> I'm assuming you mean using 'std.gc.disable()' when you say 'no gc
>> active'.
>>
>> In that case, nothing is collected.  If you run out of memory, then you
>> get an OutOfMemory exception.
> 
> I wasn't actually, because I was under the (now seemingly misguided)
> impression that the garbage collector wasn't initialized by default. And
> also the impression that it would be practical attempting to live
> without it. :P
> 

Well, I'm not saying it's impossible; I've never tried to disable the
GC.  I've seen a few modified versions of Phobos lying around that
completely omit the GC.

Perhaps a compromise would be to see if you can live without it in
*your* code, and then if you need the performance, create a specialized
version of the standard library.

Also, I'm only ~30% sure on this, but I think Ares (as mentioned above)
avoids using the GC.  That might be worth taking a gander at.

>> If you don't already know: the D gc NEVER collects objects until you run
>> out of memory.  That's when it does a collection.  One suggestion is to
>> make a very low-priority thread that runs in the background, calling
>> 'std.gc.fullCollect()' over and over again to keep your memory profile
>> from growing too large.
> 
> Ok. I'd interpreted "It is not predictable when a collection gets run,
> so the program can arbitrarily pause" (from
> http://www.digitalmars.com/d/garbage.html) as meaning that it could just
> 'decide' to kick in upon any operation that allocates memory, even if
> you weren't running low.
> 
> From the same page: "All threads other than the collector thread must be
> halted while the collection is in progress."
> That seems a little worrying. Wouldn't this mean that running it in a
> low-priority thread wouldn't actually achieve much for a game? i.e. if
> the GC decides it wants to do some huge cleanup, then there's still no
> way to have it broken down, since the GC thread won't relinquish control
> until it's done?

Hmm.  Funny, because I got the "low-priority thread" idea from
http://digitalmars.com/d/phobos/std_gc.html

(Checks)  Aah, I had missed this little tidbit: "...so that if the
program is idly waiting for input, memory can be cleaned up."  Bugger.

I wonder if it's possible to write a thread-safe collector :P

> Do you know if generational garbage collection using genCollect() would
> help, or is this just as likely to be arbitrarily 'greedy' for time? If
> not, is there an ideal solution for incremental garbage collection that
> could be built into D, or would this kill the GC's effectiveness
> completely?

If I remember correctly, genCollect() isn't implemented yet.  That, or
it's the same as calling fullCollect().

> 
>> One idea I once had was to keep an eye on the amount of memory your game
>> is using.  At start up, compute (MaxMemory - CurrentlyUsedMemory).  If
>> you're using between 0% - 20% of that, don't bother collecting.  If
>> you're using 20% - 40%, run a very-low priority background collect.  If
>> you get to 40% - 60%, run a higher-priority collect, etc.
>>
>> That way, the time spent running collections is proportional to how much
>> memory you're using.  The cost of running the GC is amortized over time,
>> instead of being in one great big lump [2].
> 
> Ok, re-thinking my above comment: does this mean that by continually
> forcing a full collection in a low-priority thread I could guarantee
> that individual garbage collections aren't going to take long? If so,
> wouldn't it then be feasible to force it constantly (every frame of the
> game) in my main thread anyway?
> 
> [noted, snipped]
> 

I'm guessing from how the sentence was phrased that the GC isn't
thread-safe.  When you think about it, it really couldn't be; otherwise
other running threads could screw up the collect.

What would be worth doing is maybe running some benchmarks to see how
memory usage/amount of garbage affects the length of collects.  If it's
low enough, then maybe running a quick collect between every few frames
might work... dunno tho.

Accursed real-world facts!  They always have to ruin good ideas :P

>>
>> Anyway, I hope this has helped, or at least given you some ideas.  Since
>> I'm still learning D myself, take all of the above with a grain or two
>> of salt.
>>
> 
> Ok, I'll keep that in mind. I've even had the audacity to question some
> of your advice above! ;)
> 
> Thanks for all your help; I appreciate your time, and sympathize with
> your blue-screen nightmares! :P

Well, hope I didn't confuse you too much with the threaded collects
thing.  Like I said, grain of salt :)

	-- Daniel

-- 

v1sw5+8Yhw5ln4+5pr6OFma8u6+7Lw4Tm6+7l6+7D
a2Xs3MSr2e4/6+7t4TNSMb6HTOp5en5g6RAHCP    http://hackerkey.com/



More information about the Digitalmars-d mailing list