Understanding Blitz Memory Thread

BlitzMax Forums/BlitzMax Programming/Understanding Blitz Memory Thread

Sean Doherty(Posted 2006) [#1]
I'm going to see if I can figure out a little about how Blitz handles memory in this thread. The following is the test first principle, I recommend using it (especially with 3rd party libs):

GCCollect()
Print "Initial Memory = " + GCMemAlloced()

'Test the Splash Screen Create.
For i = 1 To 100
	
	pIScreen = TSplashScreen.Create(pTGameController)
	pIScreen.Destroy()
	
Next

GCCollect()
Print "After GCGollect = " + GCMemAlloced()


Output:

Initial Memory = 38008
After GCGollect = 38008

No leaks.:)


Sean Doherty(Posted 2006) [#2]
Ok, but right off the bat this doesn't make sense to me? I actually use a singleton "design pattern" to load my images and maintain a pointer to the image. For example:

'Singleton Get Instance Function.
Function GetInstance:TGuiBank()
	If g_pTGuiBank= Null
		g_pTGuiBank = New TGuiBank
	End If
		
	Return g_pTGuiBank			
End Function

'Load the Splash Image.
Method LoadSplashImage:TImage()

	If m_pTSplashImage = Null
		m_pTSplashImage = LoadImage("incbin::../Images/GUI/splash.png", FILTEREDIMAGE | MASKEDIMAGE) 
	End If
		
	Return m_pTSplashImage			
		
End Method


This mean, the splash screen image is still in memory. So why does it report that no additional memory is being used?

Thanks


Dreamora(Posted 2006) [#3]
I don't see what you mean sorry.

This does not show if the object holding m_pTSplashImage is still alive or not.


Sean Doherty(Posted 2006) [#4]
It is still alive because within the create the following gets called:

pTSplashScreen.m_pTLogoImage = TGuiBank.GetInstance().LoadSplashImage()


Since, pTGuiBank is global and pTSplashImage is a field of the type TGuiBank it would still be alive. So why is the memory allocation not showing? Is it loading into video memory?


tonyg(Posted 2006) [#5]
Not sure why you're making it so complicated.
Graphics 640,480
GCCollect()
Print "First "GCMemAlloced()
Local image:TImage = LoadImage("max.png")
GCCollect()
Print "Second " GCMemAlloced()
'DrawImage image , 0 , 0
'Flip
'GCCollect()
'Print "Third "GCMemAlloced()

First and second GCMemalloced() will say the same thing.
You load the image but don't use it so, my guess, is the compiler optimises and doesn't bother loading it.
Now uncomment the last 4 lines and notice that, now the image is used, second gcmemalloced has increased.
That's my theory anyway.


H&K(Posted 2006) [#6]
Im of the opinion that any code you attach to a global field declaration, isnt run until the first instance of that type is created. (if thats a help)

But I dont understand your explination of what your doing at all. So probably It wasnt helpful


Sean Doherty(Posted 2006) [#7]
@tonyg, I'm not making it complicated; I'm using the code from my game. It is designed to only load the image into memory once no matter how many times you call the load.

@H&K, if that were true i don't think I see any allocation of any of the objects unless I was rendering. For that matter, what if you load and object and then use midhandleimage on it?

I wish there was some documentation on how memory allocation is being handled so we didn't have to guess. I'll try a few simple examples when I get a few minutes.


Sean Doherty(Posted 2006) [#8]
Check this out!

Using a global variables:

SuperStrict

graphics 1024,768,0

'Declare Variables.
Global pTImage:TImage = Null 

GCCollect()
Print "Initial Memory = " + GCMemAlloced()

pTImage = LoadImage("Images/GUI/splash.png")
'pTImage = LoadImage("Images/GUI/loading.png")
MidHandleImage(pTImage)

GCCollect()
Print "Memmory After LoadImage = " + GCMemAlloced()

'DrawImage pTImage , 0 , 0
'Flip
'GCCollect()
'Print "Third "GCMemAlloced()

Delay(1000)


Output:
Initial Memory = 37456
Memmory After LoadImage = 37624

Using a local variable:
graphics 1024,768,0

'Declare Variables.
Local pTImage:TImage = Null 

GCCollect()
Print "Initial Memory = " + GCMemAlloced()

pTImage = LoadImage("Images/GUI/splash.png")
'pTImage = LoadImage("Images/GUI/loading.png")
MidHandleImage(pTImage)

GCCollect()
Print "Memmory After LoadImage = " + GCMemAlloced()

'DrawImage pTImage , 0 , 0
'Flip
'GCCollect()
'Print "Third "GCMemAlloced()

Delay(1000)


Output:
Initial Memory = 37492
Memmory After LoadImage = 37492

Explain that?


Curtastic(Posted 2006) [#9]
It just takes some time to update the amount of memory allocated.

Try this one with waitkey.
The longer you wait before pressing a key the more memory is allocated. (until it allocates about 12000 bytes) Remove waitkey and it says nothing is allocated.
Graphics 640,480,0

GCCollect()
Print "Initial Memory = " + GCMemAlloced()

Local a[10000]

WaitKey()

GCCollect()
Print "Memmory After = " + GCMemAlloced()





Sean Doherty(Posted 2006) [#10]
Why would it take time?


tonyg(Posted 2006) [#11]
Explain that?

Is it because, now global, the compiler doesn't know what other linked program might access that image so *has* to include the loadimage?
If you run...
Graphics 640 , 480
GCCollect()
Print GCMemAlloced()
Local image:TImage = LoadImage("thisimageexists.png")
' global image:TImage=loadimage("thisimagedoesnot.png")
GCCollect()
Print GCMemAlloced()

you'll notice they use the same mem allocation.
Notice in debug mode that the image is ALWAYS loaded and the memory always changed.
It makes sense to me but it's all theory.


Dreamora(Posted 2006) [#12]
Short note on local: Those work a little different than global. Global -> System RAM
local -> CPU memory located if there is room for it

So unless you flood the cache, it won't be in RAM.

But I think the error / missassumption you are making is: Loading image -> whole image in RAM and not only the reference (ie 4bytes)
Because the image will normally reside in VRAM

You would need to create something RAM based like an array etc.


DStastny(Posted 2006) [#13]
I have examined the memory management code pretty extensively trying to determine if it would be possible to make the existing memory manager thread safe.

From what I see in code I think I can explain how it works. Mark, maybe kind enough to correct my anaysis since for some of it you have to look at the assembly genrated by the BMAX compiler to understand, and I may be intepreting the assembly code incorrectly.

First off the difference between global and local variables.

Global Variables
Are allocated at assembly time in the Data Segment of the process by the Assembled code.

Local Variables
Are allocated on the Stack when declared and are allocated at run-time. Not the CPU Cache. as Dremora states.

The algorimth for BlitzMax is a Hybrid Mark/Sweep + reference Counted Garbage Collector. It is also considered a Consevative collector as it only will call GCCollect automatically when you allocate memory. ie. New Object. See http://www.iecc.com/gclist/GC-faq.html for details about GC's.

So if you allocate a Global Var and assign it to a type the reference count of object stored in variable is increased. The memory is in the HEAP mangage by the GC. If you call GCCollect it scans the pointers allocated on the heap. If the reference count is > 0 it marks it, so it wont be released. When you set the variable to null it reduces the reference and if zero Free's the memory.

Local variables are a bit different in that they are on the Stack, if you allocate a Local Variable it doesnt use the reference count, unless you assign it to another variable. These get cleaned up by the GC by scanning the Stack and the registers of the CPU. The garbage collector scans the Regsiters and the stack for references to live objects in the HEAP. If the address is in the stack or register it Marks the Object as still alive. This is why when you set a local variable to null and call Collect it might not get collected as there may still be reference in a register.

Overall its a fairly effecient Garbage Collection schema. Based upon the way its structured currently with the reference counting done by compiler for globals there is no way to make it thread safe, with out changing the compiler.

There is more too it in the way Freelists are handled but that is not so much a GC function as a Memory Managment function but the basics are outlined above.

I think Mark goal has been balancing pretty well the convenice of a Garbage Collector vs speed of manual memory managment. Ie. New/Delete. Although I dont care for the limitation of the reference counting portion of it, vs a pure Mark/Sweep.

I have been considering playing around with this replacing the Blitzmax Collector with this one.

http://www.hpl.hp.com/personal/Hans_Boehm/gc/

It is a Mark/Sweep that implements Generational or Incremental Collection. This one would stubb out the refrence counting and would allow for cyclical references.

Tests out pretty well in C/C++ but cant get a good performance test in BMAX without coding it in. It can also be made thread-safe. Although being thread-safe would reduce performance.

This is heavy Computer Science on the links I provided but I think they can give you a good idea on how it works.

Doug Stastny


Curtastic(Posted 2006) [#14]
Hmm, move the mouse to increase memory allocated...
Graphics 640,480,0

Local a[10000]

Repeat
	GCCollect()
	DrawText GCMemAlloced() , 1 , 1
	Flip
	Cls
Until KeyHit(key_escape)


I'm wondering whats a good way to get the real amount of memory allocated? (ie without just moving the mouse around until the number stops going up)?


Sean Doherty(Posted 2006) [#15]
I've asked this before and I was hold no; but I figure it doesn't hurt as ask again. Is there anyway to looks at the reference count? I would like to see what objects Max is holding on to; rather than blindly trying to fix these allocation issues.

Thanks


tonyg(Posted 2006) [#16]
I'm guessing the problem with autoGC is not knowing what it's doing and when.
Personally, as long as memory allocation is not simply increasing without ever fluctuating then it seems OK.
Are there any solidly recreatable leaks or is it a worry that there's some stuff hanging around which should have been reclaimed but isn't?
.
You could set GCSetMode to 4 although not sure how useful the output is.


Dreamora(Posted 2006) [#17]
Sean: No there isn't any way to do that. This data is not exported as users would tend to write their own "delete fake method" or other strange stuff like this for sure.

tonyg: I don't know of any memory leaks anymore, most have been fixed between 1.06 and 1.16 (string had some funny leaks).
Most so called memory leaks that appear are by unexperienced users that create CStrings and forget to free them or that create memory blocks and forget to free them, which both is not GC managed and thus no leak


DStastny(Posted 2006) [#18]
Code to see the reference count.

Strict

Type TestObject
  Field Blah : Int=66
End Type


Function Count(o:object)
 local p:int ptr = int Ptr(byte ptr o)-1
 const BBZERO:Int=-2147483648
 Print p[0]-BBZERO
end function

Function Showlocal()
  Print "Local Reference Counting"
  local l:TestObject= New TestObject
  Count(l)
  local l2:TestObject=l
  Count(l)
end function

Function ShowGlobal()
 Print "Global Reference Counting" 
 Global g:TestObject= New TestObject
 Count(g)
 Global g2:TestObject =g
 Count(g)
end Function

ShowLocal()
Showglobal()


Here is code to show you but this example demonstrates what I was describing with Locals vs Globals it doesnt work the same way. Locals are on stack not reference counted Globals are.

There is really no useful way to track memory usage with reference count checking.

Unfortunatly with GC you really have to trust its working right. Biggest mistake with BMAX you can make is cyclical references or using handles.

Doug Stastny


Sean Doherty(Posted 2006) [#19]
Are there any solidly recreatable leaks or is it a worry that there's some stuff hanging around which should have been reclaimed but isn't?


Stuff is just not being reclained, mostly because of cyclical references that are not being cleaned up properly. Man, I miss C++, when you told it to delete something it did!


tonyg(Posted 2006) [#20]
Have you got a link to any posts with more info about the non-reclaim problems?
If not, can you post an example?


Dreamora(Posted 2006) [#21]
Sean: Main problem about C++ is that if you don't tell it to delete it does not ... and this small little thing crashes win98 / winME that often that C++ programmers really should get send to moon sometimes :-)

Main not reclaim problem happens when you set a TList to null without clearing it before ... for some reason the delete method on TLists is never called so all stuff saved within the list will never be freed again as the whole list is saved as cyclic linked list and can't be accessed anymore (so the whole thing keeps itself alive on its own).
the only way to solve the cyclic reference problem would be the addition of a root link counter, that checks if the object can be accessed at all. (at least he only solution I know of and which is normally used)


tonyg(Posted 2006) [#22]
Dreamora, you keep posting that but I'm not sure why.
Am I missing something...
Type ttest
	Global testlist:TList = CreateList()
	Field x
End Type
Print GCCollect()
Print "Before : " + GCMemAlloced()
For x = 0 To 20000
	mynew:TTest = New ttest
	ListAddLast ttest.testlist , mynew
Next
Print GCCollect()
Print "During : " + GCMemAlloced()
ttest.testlist = Null
Print GCCollect()
Print "After : " + GCMemAlloced()


?


Dreamora(Posted 2006) [#23]
Its possible that it was fixed with one of the many syncmods last week. (once to twice a day some kind of fix came in)

the main problem about this freeing problem is, that it isn't possible to "securely recreate" it, sadly, as its the GC that is either calling the delete or not. But the problem isn't coming up that often now, only now and then.
In a case it isn't calling it, clear isn't called automatically on the list as well, which is why I'm going the secure way of calling it manually before null-ifying a list. it does not cost anything but gives me the guarantee that the tlinks are gone.


tin(Posted 2008) [#24]
hi Sean Doherty

I understand why you want to use that Singleton Design Pattern. It's a good habit to use DP in programming anyway.

If i am not mistaken, all the reply to you especially (tonyg) is that, although you load image to memory, and your debug log still showing same memory amount is because of the Code Optimization Feature of the BlitzMax Compiler itself.

let's see how it work here.
GCCollect()
Print "Initial Memory = " + GCMemAlloced()
    pIScreen = TSplashScreen.Create(pTGameController)
GCCollect()
Print "Initial Memory = " + GCMemAlloced()

if you are a compiler and you do Optimization work as compiler, you know that, TGameCOntroller has created and never used, so you can simply take it out and your Optimized code will become as follow
GCCollect()
Print "Initial Memory = " + GCMemAlloced()
GCCollect()
Print "Initial Memory = " + GCMemAlloced()

the compiler has removed ( pIScreen = TSplashScreen.Create(pTGameController)) because it's simply doing nothing actually, and it will give same result with it or without it.
it's called code optimization. (Code Optimization will most probably work on Machine Opcode level.)

so you better create that object, make sure to show that object's image on the screen to make it sure code optimization will not neglect that object. and check the results.


GfK(Posted 2008) [#25]
I'd hazard a guess that he's sorted it by now, since this thread is over a year old.

Its not good practice to keep digging up ancient threads like you're doing. You've only made ten posts and this is the third time you've done it.