Memory management tutorial

BlitzMax Forums/BlitzMax Tutorials/Memory management tutorial

marksibly(Posted 2005) [#1]
Here's a brief tutorial directed mainly at BlitzPlus/Blitz3D users who are a bit confused by BlitzMax's memory management...


***** BlitzMax Memory Management - or, 'where the @*#! did Delete go?!?' *****

As many users from a Blitz3D or BlitzPlus background have noticed, BlitzMax does not include a 'Delete' command.

This appears to have caused a great deal of bemusement and head scratching in the Blitz community, which this tutorial will attempt to clear up.

Note that any references to Blitz3D in the rest of this document also apply to BlitzPlus.


The BASICS
----------

First up, let's have a look at what Blitz3D's Delete command actually does. It really serves 2 purposes:

1) The object is removed from its 'type list'.

2) The memory used by the object is returned to the system - this is called 'deallocating' the object.

Step 1 is necessary due to Blitz3D's type list system, which automatically adds all New-ed objects to their global linked list.

Step 2 is necessary to prevent memory leaks. A memory leak is when your program has allocated a chunk of memory, used it for something, and then forgotten to deallocate it when it is no longer useful. Since computers have a finite amount of memory, this will eventually cause problems if it happens often enough!

The first thing about these 2 steps to note is that the global type list system is no longer present in BlitzMax, so step 1 is now completely up to the user - if you have added an object to a linked list (or lists), it is up to you to remove it.

If all you are wanting to achieve is a system similar to Blitz3D's type list system, then this is as simple as using ListAddLast immediately after you have created an object using New, and using ListRemove immediately before deleting it using Delete - oops, there is no Delete! Ok, using ListRemove *instead* of Delete!

This still leaves the question of deallocating the object's memory, and this is done in BlitzMax using the FlushMem command. The basic idea behind FlushMem is that it automatically detects 'dead' objects - objects that are no longer in use by your program - and deallocates them for you.

FlushMem should be placed in your program's 'main loop', and perhaps at other 'hotspots' in your program where you have created a lot of objects - for example, after loading a game level.

Note that the issue of deallocating objects is completely separate from the issue of type lists. Yes, if you forget to remove an object from a list it will remain 'live', but this is the same as forgetting to delete an object in Blitz3D!

One final complicating issue: BlitzMax includes a feature that allows you to assign objects to integers. This feature exists mainly to simplify things for beginners so, instead of...

Local image:TImage=LoadImage( "somepic.png" ) 'normal object to object assignment...

...you can just go...

Local image=LoadImage( "somepic.png" ) 'assigning an object to an int! What gives?

However, if you are using this feature in your programs, you *must* later 'free' the object yourself using Release...

Release image

You can therefore think of Release as being similar to Blitz3D's FreeImage (or FreeSound, FreeThis, FreeThat etc).

All-in-all, it's probably best to avoid object-to-int assignments altogther if you are concerned about memory leaks.

And that's it! Well, not quite, but the above should be enough information for you to be able to write leak-free programs with. I'll go into some of the nitty gritty details below...


***** The nitty gritty part 1 : dead objects *****

Ok, lets have a look at the question of 'what exactly is a dead object'? Well, a dead object is an object that is no longer referred to by any live objects. In other words, dead objects are objects your program can no longer 'see' - they are therefore completely useless to your program and can be safely deallocated.

Here are a few examples of when objects become 'dead':

'example 1
Local p:MyType=New MyType 'allocate object 1
p=Null 'object 1 is now dead

'example 2
Local p:MyType=New MyType 'allocate object 1
Local q:MyType=New MyType 'allocate object 2
p=q 'object 1 is now dead, object 2 is still alive

'example 3
Local p:MyType[1] 'allocate object 1 (an array object)
p[0]=New MyType 'allocate object 2
p=Null 'object 1 and object 2 both now dead

'example 4
Local p:MyType=New MyType 'allocate object 1
p.my_type=New MyType 'allocate object 2
p.my_type=New MyType 'allocate object 3 - object 2 now dead
Local q:MyType=p.my_type
p=Null 'object 1 and object 2 dead, object 3 still alive thanks to the q assignment above

After seeing this sort of thing, there is a tendancy for people to stick '=Null' code all over the place in an attempt to 'force' objects to become dead - but this is generally not necessary.

Sooner or later, all variables will either be overwritten by new values, or (in the case of Locals and Fields) simply go out of scope. Both of these actions are enough to ensure that dead objects will be correctly detected and deallocated.

Probably the only good reason to use '=Null' is in the case of global variables, when you know that a global variable will not be modified for a long time, but it also happens to be keeping a large chunk of memory alive that is no longer useful to your program. In this case, an '=Null' is probably the right choice.

Finally, there is one subtly with FlushMem you should be aware of: FlushMem only deallocates objects which have become dead since the function the FlushMem appears in was entered.

For this reason, it pays to put FlushMem as 'low' in your code as possible. For most applications, inside the 'main loop' is fine.


***** The nitty gritty part 2 : Tracking memory allocation *****

BlitzMax provides the MemAlloced() function to determine the amount of memory currently used by your program. However, this should be used with care.

The best place to use MemAlloced() is inside the 'meatiest' loop in your program. The important thing is that your program does not lose memory over time, and simply using MemAlloced() after executing a few instructions is not generally a good indicator of this. This is because some commands may perform 'first time initialization' allocations - indeed, the BlitzMax runtime occasionally has to reallocate memory itself, to adapt to the needs of your program.

Here is an example of the 'cleanest' way to use MemAlloced()

'MemAlloced() example:
Strict

Function Test()
'code you want to test for mem stability here...
End Function

For Local k=1 To 10000
Test
FlushMem
Print MemAlloced()
Next




semar(Posted 2005) [#2]
Thank you Mark, that's really useful.

Sergio.


Perturbatio(Posted 2005) [#3]
you should stick this in the wiki Mark :)


SillyPutty(Posted 2005) [#4]
yeah, this topic is extremely important and useful ! :)


EOF(Posted 2005) [#5]
Does Flushmem use much CPU time? Let's say for example, there are several hundred variables to clear out?

Is it generally better to 'flushmem' every loop or wait a few cycles then do the flush?

When your program closes does Max clean up memory if you forget to take care of the memory management yourself?

Is it correct to say that Flushmem works locally?


BlitzSupport(Posted 2005) [#6]
Just do it every loop. FlushMem works locally in that it clears anything from the program section it's in, plus stuff called from that section, eg.

Repeat

    Cls

    GetControls ()
    RenderGame ()

    FlushMem
    Flip

Until KeyHit (KEY_ESCAPE)


This would clear everything in (and before) that Repeat/Until loop, plus the stuff in the two functions. You'd only need to put FlushMem inside those functions if they do a large amount of object creation/nulling or particularly heavy loops. That's really just a matter of judgement/trial and error.

Max frees stuff at the end if you don't.