Object Pooling

BlitzMax Forums/BlitzMax Programming/Object Pooling

DStastny(Posted 2006) [#1]
There has been alot of questions about Garbage Collection, in BMAX and people looking for or worrying about leaks.

One thing not discussed is performance. This is not necessarily issue with Garbage Collection but more of Blitz Max being object based language. i.e. Everything being object. As such it becomes quite easy to start new'ing objects all over your code and quickly peformance will start to drag.

I thought I would share a common technique that can be useful in mitgating performance issues caused by allocations. If you learn one thing from this tutorial learn this Memory Allocations are evil!

The technique I am going to demonstrate is known as object pooling and the performance increase can be dramatic, as it ensures that the Garbage collector is never invoked, and more specifically no-allocations have to happen.

The following example code demonstrates the mechanism and pretty much any object can be extended to use it. It does however require that you manage the life-time of your objects ala C/C++ or other structured languages however the benfit is well worth it.

To see it work best compile and run in non-debug mode.
SuperStrict
' This is simple program to demostrate concept of Object Pooling.  
' This is common technique used in Object and Garbage collected Languages
' to maximize performance and to minimize possiblitiy of Garbage Collector
' from being invoked in real time processes.  This is technique commonly used
' in Java,.NET Languages and even C/C++ with Garbage Collectors
'
' Number of loops for our test cases
Const MAXITS:Int=2000000

' these are used to ensure the BMax Compiler does not optimize our loops
' The Blitz Max Compiler is pretty smart and will optimize out code and variables
' If it detects its not begin used so this helps trick it :)

Global TotalX:Float
Global TotalY:Float

' Important to Note: That during our tests we dont use any Strings
' Strings are GC'd by nature so simple string manipulations can and will invoke GC
' If you need strings just be sure you allocate them up front and you will be ok
' Just dont do things like this:  
' FPS$= "FPS="+framespersec 
' DrawText 0,0,FPS$

' Now on to test object
' simple 2d Vector type
Type TVector2d
	Field x : Float
	Field y : Float
	
	Function Create:TVector2d(x:Float=0.0,y:Float=0.0)
		Local v:TVector2d = New TVector2d
		v.x=x
		v.y=y
		Return v
	End Function	
End Type	

' Test loop can just adds up some simple vectors to simulate work
Function TestNonPooled:Int()
	Local time:Int=MilliSecs()
	For Local i:Int = 0 To MAXITS
    	Local v1:TVector2d = TVector2d.Create(0.0,0.0)
		Local v2:TVector2d = TVector2d.Create(1.0,1.0)		
		Local v3:TVector2d = TVector2d.Create(v1.x+v2.x,v1.y+v2.y)
		TotalX=TotalX+v3.x
		TotalY=TotalY+v3.y		
	Next	
	Return MilliSecs()-time
End Function

' Now we will create a Pool of Vectors to minimize impact caused by garbage collection and 
' overhead of allocations.  This is accomplished by making simple linked list
' to keep track of available vectors if none are available we will allocate one
' if we forget to call Free thats ok it will then be garbage collected.

Type TVector2dPool Extends TVector2d
    Global _Freelist : TVector2dPool 
    Field _Next : TVector2dPool

    ' The create function looks to see if object is available in
    ' the pool
    Function Create:TVector2dPool(x:Float=0.0,y:Float=0.0)
      	Local v : TVector2dPool

	  	If (_Freelist)
	  	    ' Have a free object in pool
	    	v = _Freelist;
	    	_Freelist = v._Next;	  	
		Else
		    ' Make a new one, this might invoke GC
	 		v = New TVector2dPool
		End If			
		v.x=x
		v.y=y
		Return v;
    End Function

    Method Free()
       ' Adds the object back to freelist
    	Self._Next = _Freelist;
	    _Freelist = Self;
    End Method  
    
    Function InitializePool(Size:Int)
        ' Free the current pool if one exists
    	TVector2dPool._Freelist=Null    	
    	' all we do is create and free to grow the pool
		For Local i:Int=0 Until Size 
			TVector2dPool.Create().Free()
		Next    	
    End Function
End Type

' Initialize Pool
' For our test we will just put in 100 objects we only need 3.
' This is good idea to do before hand. If we are wrong with 100 thats ok it will grow
' if more are needed however we face the rath of allocation and GC so good idea to plan
' ahead for amount you need

TVector2dPool.InitializePool(100)

' Same Test now using pooled objects that we Free when finished
	
Function TestPooled:Int()
	Local time:Int=MilliSecs()
	For Local i:Int = 0 To MAXITS
		Local v1:TVector2dPool = TVector2dPool.Create(0.0,0.0)
		Local v2:TVector2dPool = TVector2dPool.Create(1.0,1.0)		
		Local v3:TVector2dPool = TVector2dPool.Create(v1.x+v2.x,v1.y+v2.y)
		TotalX=TotalX+v3.x
		TotalY=TotalY+v3.y							
	    v1.Free()
	    v2.Free()
	    v3.Free()	
	Next	
	Return MilliSecs()-time
End Function

' now we run our tests
' we will GCCollect to ensure the GC is in cleared state before our tests
GCCollect()
Local timeNonPooled:Float=TestNonPooled()
Print "Test 1(NonPooled) complete X="+TotalX+", Y="+TotalY
' we will GCCollect to ensure the GC is in cleared state before our tests
TotalX=0.0
TotalY=0.0
GCCollect()
' Lets see the results
Local timePooled:Float=TestPooled()
Print "Test 1(Pooled) complete X="+TotalX+", Y="+TotalY
Print "--------------"
Print "Results"
Print "Time for Tests"
Print "Test 1(No Pool)="+timeNonPooled
Print "Test 2(Pooled)="+timePooled
Print "--------------"
Print "Test 2(Pooled) is "+timeNonPooled/timePooled*100+ "% faster"
Input "Enter for Exit"
End


Hopefully some of you will find this useful, this technique is common and has been used by developers for decades. Old days pre-OOP we would create seperate heaps for various structures. So network packets dont get mixed with graphics data, etc.

Have fun
Doug Stastny


dan_upright(Posted 2006) [#2]
this has been a big help for me - i knew i had to watch my string use but i'd never considered that i should be watching my object use too

thanks a lot =]


impixi(Posted 2006) [#3]
Yes indeed. Thanks Budman.


Defoc8(Posted 2006) [#4]
jst one thing - wont "Test 1(Pooled) complete X="+TotalX+", Y="+TotalY
actually cause string objects to be created on the fly? - i know that the string literals in this expression will most
probably be allocated on startup..but the addition operator,
will most probably cause dynamic mem alloc's.
Then again i dont know enough about how bmax works
under the hood..so im probably mistaken ;)

heh..anyway, it doesnt make your point any less valid.


DStastny(Posted 2006) [#5]
In the case of the Print Statements it would, but for the test which is timed around the function I needed someway to show the feedback from the tests :).

Doug Stastny