I’ve previously covered ways of implementing in my article on Runnables (aka observers) which showed how to call back about 15 times faster than just using a Function object. There are still more ways to call back though and I didn’t cover them at the time. Today I’ll be adding to Function and Runnables by testing Event and the as3signals library by Robert Penner.

As you most certainly know, AS3’s ubiquitous Event class is a way of call back a whole collection of functions at the same time. These have numerous problems ranging from performance to object allocation and garbage collection to more stylistic concerns. This prompted Robert Penner to create the as3signals system as a replacement for Events. Today I will test both as3signals and Events against simple Vectors of Functions and Runnables. I will also test as3signals and Events against a single Function and a single Runnable. The purpose of this is to show how various levels of complexity and features affect performance. Clearly as3signals and Event are far more advanced than a single callback or a simple Vector of callbacks, but sometimes that’s all you need. That said, let’s take a look at the test app:

package
{
	import flash.display.*;
	import flash.events.*;
	import flash.text.*;
	import flash.utils.*;
 
	import org.osflash.signals.*;
 
	[SWF(backgroundColor=0xEEEADB,frameRate=1000)]
 
	/**
	*   A test of various callback techniques
	*   @author Jackson Dunstan
	*/
	public class CallbacksTest extends Sprite
	{
		public var signal:Signal;
		public var funcs:Vector.<Function> = new Vector.<Function>();
		public var runnables:Vector.<Runnable> = new Vector.<Runnable>();
 
		public function CallbacksTest()
		{
			var logger:TextField = new TextField();
			logger.autoSize = TextFieldAutoSize.LEFT;
			addChild(logger);
 
			this.signal = new Signal();
 
			var i:int;
 
			const FUNC_CALL_REPS:int = 1000000;
			const NUM_LISTENERS:int = 10;
			const EVENT_TYPE:String = "test";
			var runnable:Runnable = new MyRunnable();
			var func:Function = runnable.run;
 
			// Single call
 
			var beforeTime:int = getTimer();
			for (i = 0; i < FUNC_CALL_REPS; ++i)
			{
				func();
			}
			logger.appendText("Func call time: " + (getTimer()-beforeTime) + "\n");
 
			beforeTime = getTimer();
			for (i = 0; i < FUNC_CALL_REPS; ++i)
			{
				runnable.run();
			}
			logger.appendText("Runnable call time: " + (getTimer()-beforeTime) + "\n");
 
			addEventListener(EVENT_TYPE, onEvent0);
			beforeTime = getTimer();
			for (i = 0; i < FUNC_CALL_REPS; ++i)
			{
				dispatchEvent(new Event(EVENT_TYPE));
			}
			logger.appendText("Event (1 listener) time: " + (getTimer()-beforeTime) + "\n");
			removeEventListener(EVENT_TYPE, onEvent0);
 
			this.signal.add(onSignal0);
			beforeTime = getTimer();
			for (i = 0; i < FUNC_CALL_REPS; ++i)
			{
				this.signal.dispatch();
			}
			logger.appendText("Signal (1 listener) time: " + (getTimer()-beforeTime) + "\n\n");
			this.signal.remove(onSignal0);
 
			// Calling a list
 
			for (i = 0; i < NUM_LISTENERS; ++i)
			{
				this.funcs.push(func);
			}
			beforeTime = getTimer();
			for (i = 0; i < FUNC_CALL_REPS; ++i)
			{
				dispatchFuncs(this.funcs);
			}
			logger.appendText("Func call (" + NUM_LISTENERS + " listeners) time: " + (getTimer()-beforeTime) + "\n");
 
			for (i = 0; i < NUM_LISTENERS; ++i)
			{
				this.runnables.push(runnable);
			}
			beforeTime = getTimer();
			for (i = 0; i < FUNC_CALL_REPS; ++i)
			{
				dispatchRunnables(this.runnables);
			}
			logger.appendText("Runnable call (" + NUM_LISTENERS + " listeners) time: " + (getTimer()-beforeTime) + "\n");
 
			for (i = 0; i < NUM_LISTENERS; ++i)
			{
				addEventListener(EVENT_TYPE, this["onEvent"+i]);
			}
			beforeTime = getTimer();
			for (i = 0; i < FUNC_CALL_REPS; ++i)
			{
				dispatchEvent(new Event(EVENT_TYPE));
			}
			logger.appendText("Event (" + NUM_LISTENERS + " listeners) time: " + (getTimer()-beforeTime) + "\n");
 
			for (i = 0; i < NUM_LISTENERS; ++i)
			{
				this.signal.add(this["onSignal"+i]);
			}
			beforeTime = getTimer();
			for (i = 0; i < FUNC_CALL_REPS; ++i)
			{
				this.signal.dispatch();
			}
			logger.appendText("Signal (" + NUM_LISTENERS + " listeners) time: " + (getTimer()-beforeTime) + "\n");
		}
 
		private function dispatchFuncs(funcs:Vector.<Function>): void
		{
			var len:int = funcs.length;
			for (var i:int = 0; i < len; ++i)
			{
				funcs[i]();
			}
		}
 
		private function dispatchRunnables(runnables:Vector.<Runnable>): void
		{
			var len:int = runnables.length;
			for (var i:int = 0; i < len; ++i)
			{
				Runnable(runnables[i]).run();
			}
		}
 
		private function onEvent0(ev:Event): void {}
		private function onEvent1(ev:Event): void {}
		private function onEvent2(ev:Event): void {}
		private function onEvent3(ev:Event): void {}
		private function onEvent4(ev:Event): void {}
		private function onEvent5(ev:Event): void {}
		private function onEvent6(ev:Event): void {}
		private function onEvent7(ev:Event): void {}
		private function onEvent8(ev:Event): void {}
		private function onEvent9(ev:Event): void {}
 
		private function onSignal0(): void {}
		private function onSignal1(): void {}
		private function onSignal2(): void {}
		private function onSignal3(): void {}
		private function onSignal4(): void {}
		private function onSignal5(): void {}
		private function onSignal6(): void {}
		private function onSignal7(): void {}
		private function onSignal8(): void {}
		private function onSignal9(): void {}
	}
}
internal interface Runnable
{
	function run(): void;
}
internal class MyRunnable implements Runnable
{
	public function run(): void {}
}

Here are the results I get for the single listener versions:

Environment Func (1) Runnable (1) Event (1) Signal (1)
3.0 Ghz Intel Core 2 Duo, 4GB, Windows XP 67 4 1336 1412
2.0 Ghz Intel Core 2 Duo, 4GB, Mac OS X 10.5 90 7 6108 2275
2.2 Ghz Intel Core 2 Duo, 2GB, Mac OS X 10.6 82 6 5115 2057

And here are the results for the multiple-listener versions:

Environment Func (10) Runnable (10) Event (10) Signal (10)
3.0 Ghz Intel Core 2 Duo, 4GB, Windows XP 668 180 2877 2804
2.0 Ghz Intel Core 2 Duo, 4GB, Mac OS X 10.5 964 342 38643 4314
2.2 Ghz Intel Core 2 Duo, 2GB, Mac OS X 10.6 880 311 31512 3958

The above table shows that we don’t quite have a separation between the single listener tests and the multiple listener tests, which is rather disappointing. The single Runnable test is shockingly fast compared to as3signals (300x slower) and Event (300x slower on Windows, 600x slower on Mac). However, as pointed out above, these systems are much more complex and have many more features than a single callback. Event, for example, requires allocating a new Event object and supports such advanced features as bubbling, canceling, and multiple listening functions. This is also true in a comparison of a simple Function object to either the as3signals or Event system. So, assuming you need that extra power and can’t get by with just a simple callback, let’s move on to where as3signals and Event are more justified: multiple callbacks.

Here we see the winner again is a Vector of Runnables, which beats out second place Vector of Functions by about three-to-one. Then come Event and as3signals, which are practically the same speed on Windows, but as3signals is about 10x faster on Mac. This is good news for those desiring an arguably-cleaner interface than Event presents and also good news for those hoping for a speed boost by using as3signals on a Mac, but not Windows. On the contrary, it’s good news for those with significant investments in the Event system who are only targeting Windows performance: there’s no need to re-write using as3signals to get a speedup so long as you keep to Windows. You might, rather, re-write to use a Vector of Runnables though if you’re interested in a a 15x speedup on Windows and 15-100x speedup on Mac.

What you’d lose in the process of converting from Event or as3signals to a simpler approach like a Vector of Runnables is a lot of the sophistication of those approaches. You’ll lose the aforementioned bubbling and canceling, some safety regarding changes to the Vector during a dispatch operation, type safety in the case of as3signals, consistency with the Flash API in the case of the Event system, and likely many more niceties. What you gain is raw speed. Only you can choose the appropriate system for you application.