Callback Strategies
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.
#1 by John Lindquist on February 1st, 2010 ·
Mind if I ask which Flash Player version you used in each environment? Rumor has it that Flash Player 10 (or 10.1 beta) has seen significant event performance boosts compared to player 9.
#2 by jackson on February 1st, 2010 ·
I always test with the latest release of the Flash Player plugin for the latest version of Firefox. In this case I tested with Flash Player 10 and Firefox 3.6 in all environments. I’ve never tested the Event system in Flash Player 9 so I can’t comment on any speedups in Flash Player 10. Drop me a line if you do such a test and I’ll post the results here.
#3 by jpauclair on February 1st, 2010 ·
I use Runnable class for Java threading all the time but never used that kind of strategy for flash… *shame on me*
This is pretty cool! Very nice post! thanks Jackson!
#4 by Robert Penner on February 1st, 2010 ·
Thanks for putting this together, Jackson. I’ve found that a slow part of Signal.dispatch() is cloning the array of listeners before iterating. If I remove that, the test is almost 3 times faster on Windows.
#5 by jackson on February 1st, 2010 ·
That’s what I figured too. I briefly perused the source code and saw some performance-related TODOs in there near the concat() call, which seems to be in place to make sure that you call back listeners that get removed during the dispatch. Perhaps there’s a faster way, such as keeping two lists like in a back buffering strategy. Just a thought; I haven’t performance-tested such an option.
Thanks for your work on as3signals. In addition to providing a nice alternative to the Event API they have provided a very nice performance increase on Macs!
#6 by Sharedtut on February 1st, 2010 ·
Thank you for posting some code to show your example.
#7 by Karl Knocking on February 4th, 2010 ·
Wow, impressive results. Runnables seem to be the thing for me! :)
#8 by Robert Penner on February 9th, 2010 ·
I optimized Signal dispatch() by moving the listeners array clone to add() and remove(). It’s about twice as fast as before on Windows, when sending to one listener.
#9 by jackson on February 10th, 2010 ·
Cool! Perhaps I will re-test…
#10 by jackson on February 16th, 2010 ·
I updated to your latest source (Git from last night, 2/15/2010) and my re-test is part of my article Introducing TurboSignals. It does indeed look like you’ve made some serious strides in performance. Congrats!
#11 by Arnaud on February 16th, 2010 ·
Very good post ! very interesting.
What is your opinion about the MATE framework which makes an extensive use of Events ?
#12 by jackson on February 16th, 2010 ·
I hadn’t heard about the MATE framework before, but it does indeed look like it uses a lot of events. All of the examples look pretty simple though, so there’s probably not much of a performance concern at all. Perhaps a much larger, more complicated MATE application would start to feel the Event/EventDispatcher system pain. Believe me, if you look into how much allocation, deallocation, and the GC work that system produces, you’ll see why it’s bad to do a lot of it (hundredsper second). A few a second is just fine for a weather widget. :)
#13 by paul on October 25th, 2010 ·
Not sure I follow the reason for comparing generic function calling and typed function calling in the same class, or context. I only use Function types when providing a callback to out of context functions. Am I missing something?
#14 by jackson on October 25th, 2010 ·
I don’t think you’re missing anything. The title of the article is “Callback Strategies”, so it’s all about the performance of various ways of implementing callbacks. One of those ways is via a
Function
-typed variable, like you say you use, but another is to use the Runnable/observer strategy as discussed in the previous article. The Runnable strategy allows you to use typed functions as you’re calling a typed function of theRunnable
object. This typed function is the callback. This huge speed boost gives rise to my TurboSignals library, which is a very fast way of implementing an events/signals system using the Runnable/observer strategy.#15 by Paul on October 25th, 2010 ·
Thanks. I’ll check out the article.
Some context switching would be in keeping with the reason for using callbacks.
#16 by devu on May 5th, 2011 ·
Regarding to Flash and Mobile development
[…]If you are still concern about it, take a look here or try it by yourself.[…]