Proxy Performance
The Flash API has a gem of a class in Proxy. You can use it to customize the behavior of the dot (.
), index ([]
), delete
, and in
operators as well as the for-in
and for-each-in
loops. Today’s article answers a recent comment by exploring the performance implications of all this fancy customizing that Proxy
allows.
To use Proxy
, you must make a dynamic
class that extends Proxy
and overrides one or more of Proxy
‘s methods to customize the behavior of the various operations listed above. Let’s start by looking at the WithProxy
class that simply wraps a Dictionary
using Proxy
:
package { import flash.utils.Proxy; import flash.utils.flash_proxy; import flash.utils.Dictionary; public dynamic class WithProxy extends Proxy { private var __dict:Dictionary; private var __propNames:Vector.<String>; private var __numProps:int; public function WithProxy() { __dict = new Dictionary(); __propNames = new Vector.<String>(); } override flash_proxy function callProperty(methodName:*, ...args): * { var k:*; var dict:Dictionary = __dict; if (methodName == "countKeys") { var count:int; for (k in dict) { count++; } return count; } else if (methodName == "nullValues") { for (k in dict) { dict[k] = null; } } return undefined; } override flash_proxy function deleteProperty(name:*): Boolean { var ret:Boolean = (name in __dict); delete __dict[name]; return ret; } override flash_proxy function getDescendants(name:*): * { return __dict[name]; } override flash_proxy function getProperty(name:*): * { return __dict[name]; } override flash_proxy function hasProperty(name:*): Boolean { return name in __dict; } override flash_proxy function isAttribute(name:*): Boolean { return name in __dict; } override flash_proxy function nextName(index:int): String { return __propNames[index-1]; } override flash_proxy function nextNameIndex(index:int): int { if (index == 0) { var propNames:Vector.<String> = __propNames; propNames.length = 0; var size:int; for (var k:* in __dict) { propNames[size++] = k; } __numProps = size; } return (index < __numProps) ? (index+1) : 0; } override flash_proxy function nextValue(index:int): * { return __dict[__propNames[index-1]]; } override flash_proxy function setProperty(name:*, value:*): void { __dict[name] = value; } } }
And now a version that doesn’t use Proxy
– the WithoutProxy
class:
package { import flash.utils.Dictionary; public class WithoutProxy { private var __dict:Dictionary; public function WithoutProxy() { __dict = new Dictionary(); } public function countKeys(): int { var count:int; for (var k:* in __dict) { count++; } return count; } public function nullValues(): void { var dict:Dictionary = __dict; for (var k:* in dict) { dict[k] = null; } } public function deleteProperty(name:*): Boolean { var ret:Boolean = (name in __dict); delete __dict[name]; return ret; } public function getDescendants(name:*): * { return __dict[name]; } public function getProperty(name:*): * { return __dict[name]; } public function hasProperty(name:*): Boolean { return name in __dict; } public function isAttribute(name:*): Boolean { return name in __dict; } public function iterate(): Dictionary { var props:Dictionary = new Dictionary(); var dict:Dictionary = __dict; for (var k:* in dict) { props[k] = dict[k]; } return props; } public function setProperty(name:*, value:*): void { __dict[name] = value; } } }
To see the usage of these classes and to check their correctness, here’s a small app that does the same actions with both of them and displays the results side by side:
package { import flash.display.*; import flash.utils.*; import flash.text.*; public class ProxyTest extends Sprite { private var __logger:TextField = new TextField(); private function log(msg:*): void { __logger.appendText(msg + "\n"); } public function ProxyTest() { __logger.autoSize = TextFieldAutoSize.LEFT; addChild(__logger); testWithProxy(); var loggerWidth:Number = __logger.width; __logger = new TextField(); __logger.x = loggerWidth + 5; __logger.autoSize = TextFieldAutoSize.LEFT; addChild(__logger); testWithoutProxy(); } private function testWithProxy(): void { log("----- WithProxy -----"); var proxy:WithProxy = new WithProxy(); log('proxy["Jackson"] = "Dunstan"'); proxy["Jackson"] = "Dunstan"; log('"Jackson" in proxy: ' + ("Jackson" in proxy)); log('proxy["Jackson"]: ' + proxy["Jackson"]); log("proxy.countKeys(): " + proxy.countKeys()); log("proxy.nullValues(): " + proxy.nullValues()); log('"Jackson" in proxy: ' + ("Jackson" in proxy)); log('proxy["Jackson"]: ' + proxy["Jackson"]); log('delete proxy["Jackson"]'); delete proxy["Jackson"]; log('"Jackson" in proxy: ' + ("Jackson" in proxy)); log('proxy["Jackson"] = "Programmer"'); proxy["Jackson"] = "Programmer"; log("proxy..Jackson: " + proxy..Jackson); log('proxy["Circle"] = "Round"'); proxy["Circle"] = "Round"; log("for-in..."); for (var k:* in proxy) { log(" " + k + " -> " + proxy[k]); } log("for-each-in..."); for each (var adjective:String in proxy) { log(" " + adjective); } } private function testWithoutProxy(): void { log("----- WithoutProxy -----"); var proxy:WithoutProxy = new WithoutProxy(); log('proxy.setProperty("Jackson", "Dunstan")'); proxy.setProperty("Jackson", "Dunstan"); log('proxy.hasProperty("Jackson"): ' + proxy.hasProperty("Jackson")); log('proxy.getProperty("Jackson"): ' + proxy.getProperty("Jackson")); log("proxy.countKeys(): " + proxy.countKeys()); log("proxy.nullValues(): " + proxy.nullValues()); log('proxy.hasProperty("Jackson"): ' + proxy.hasProperty("Jackson")); log('proxy.getProperty("Jackson"): ' + proxy.getProperty("Jackson")); log('proxy.deleteProperty("Jackson")'); proxy.deleteProperty("Jackson"); log('proxy.hasProperty("Jackson"): ' + proxy.hasProperty("Jackson")); log('proxy.setProperty("Jackson", "Programmer")'); proxy.setProperty("Jackson", "Programmer"); log('proxy.getDescendants("Jackson"): ' + proxy.getDescendants("Jackson")); log('proxy.setProperty("Circle", "Round")'); proxy.setProperty("Circle", "Round"); log("for-in..."); for (var k:* in proxy.iterate()) { log(" " + k + " -> " + proxy.getProperty(k)); } log("for-each-in..."); for each (var adjective:String in proxy.iterate()) { log(" " + adjective); } } } }
The results look like this:
----- WithProxy ----- proxy["Jackson"] = "Dunstan" "Jackson" in proxy: true proxy["Jackson"]: Dunstan proxy.countKeys(): 1 proxy.nullValues(): undefined "Jackson" in proxy: true proxy["Jackson"]: null delete proxy["Jackson"] "Jackson" in proxy: false proxy["Jackson"] = "Programmer" proxy..Jackson: Programmer proxy["Circle"] = "Round" for-in... Jackson -> Programmer Circle -> Round for-each-in... Programmer Round ----- WithoutProxy ----- proxy.setProperty("Jackson", "Dunstan") proxy.hasProperty("Jackson"): true proxy.getProperty("Jackson"): Dunstan proxy.countKeys(): 1 proxy.nullValues(): undefined proxy.hasProperty("Jackson"): true proxy.getProperty("Jackson"): null proxy.deleteProperty("Jackson") proxy.hasProperty("Jackson"): false proxy.setProperty("Jackson", "Programmer") proxy.getDescendants("Jackson"): Programmer proxy.setProperty("Circle", "Round") for-in... Circle -> Round Jackson -> Programmer for-each-in... Round Programmer
Finally, let’s do a performance test to compare the classes:
package { import flash.display.*; import flash.utils.*; import flash.text.*; public class ProxyPerformance extends Sprite { private var __logger:TextField = new TextField(); private function row(...cols): void { __logger.appendText(cols.join(",") + "\n"); } public function ProxyPerformance() { __logger.autoSize = TextFieldAutoSize.LEFT; addChild(__logger); var beforeTime:int; var afterTime:int; var wTime:int; var woTime:int; var key:String; var val:String; var REPS:int = 100000; var SIZE:int = 100; var DELETE_KEY:String = "--keynotfound--"; var GET_KEY:String = "key0"; var SET_VAL:String = "val0"; var i:int; var wProxy:WithProxy = new WithProxy(); var woProxy:WithoutProxy = new WithoutProxy(); for (i = 0; i < SIZE; ++i) { key = "key" + i; val = "val" + i; wProxy[key] = val; woProxy.setProperty(key, val); } row("Function", "WithProxy", "WithoutProxy"); beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { wProxy.countKeys(); } afterTime = getTimer(); wTime = afterTime - beforeTime; beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { woProxy.countKeys(); } afterTime = getTimer(); woTime = afterTime - beforeTime; row("callProperty", wTime, woTime); beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { delete wProxy[DELETE_KEY]; } afterTime = getTimer(); wTime = afterTime - beforeTime; beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { woProxy.deleteProperty(DELETE_KEY); } afterTime = getTimer(); woTime = afterTime - beforeTime; row("deleteProperty", wTime, woTime); beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { wProxy..key0; } afterTime = getTimer(); wTime = afterTime - beforeTime; beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { woProxy.getDescendants(GET_KEY); } afterTime = getTimer(); woTime = afterTime - beforeTime; row("getDescendants", wTime, woTime); beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { wProxy[GET_KEY]; } afterTime = getTimer(); wTime = afterTime - beforeTime; beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { woProxy.getProperty(GET_KEY); } afterTime = getTimer(); woTime = afterTime - beforeTime; row("getProperty", wTime, woTime); beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { GET_KEY in wProxy; } afterTime = getTimer(); wTime = afterTime - beforeTime; beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { woProxy.hasProperty(GET_KEY); } afterTime = getTimer(); woTime = afterTime - beforeTime; row("hasProperty", wTime, woTime); beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { wProxy[GET_KEY] = SET_VAL; } afterTime = getTimer(); wTime = afterTime - beforeTime; beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { woProxy.setProperty(GET_KEY, SET_VAL); } afterTime = getTimer(); woTime = afterTime - beforeTime; row("setProperty", wTime, woTime); beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { for each (key in wProxy) { val = key; } } afterTime = getTimer(); wTime = afterTime - beforeTime; beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { for each (key in woProxy.iterate()) { val = key; } } afterTime = getTimer(); woTime = afterTime - beforeTime; row("for-each", wTime, woTime); beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { for each (val in wProxy) { key = val; } } afterTime = getTimer(); wTime = afterTime - beforeTime; beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { for each (val in woProxy.iterate()) { key = val; } } afterTime = getTimer(); woTime = afterTime - beforeTime; row("for-each-in", wTime, woTime); } } }
I ran the performance test with the following environment:
- Flex SDK (MXMLC) 4.5.1.21328, compiling in release mode (no debugging or verbose stack traces)
- Release version of Flash Player 10.3.183.7
- 2.4 Ghz Intel Core i5
- Mac OS X 10.7.1
And got these results:
Function | WithProxy | WithoutProxy |
---|---|---|
callProperty | 812 | 746 |
deleteProperty | 88 | 29 |
getDescendants | 32 | 10 |
getProperty | 40 | 10 |
hasProperty | 22 | 8 |
setProperty | 43 | 12 |
for-each | 3135 | 5367 |
for-each-in | 3144 | 5383 |
Let’s cut out the loops, which make it hard to see the quicker functions:
As you can see, the performance of WithProxy
is usually much worse than WithoutProxy
. To summarize:
callProperty
is only marginally slower, probably due to theif-else
chain required- Everything else is about 3-4x slower (except loops)
- Exception:
for-in
andfor-each-in
performance is about twice as fast. However, this may be due to my implementation and possible to overcome using an alternate technique. Certainly, creating a whole copy of theDictionary
(as in both cases) is not optimal.
So, you should not be using Proxy
in performance-critical code. In addition to forcing you into a slow dynamic
class, it also mandates an API that often lacks types and shoehorns you into some slow patterns like an if-else
chain for callProperty
. The Proxy
class does work though: it’s behavior is correct and it does allow you to override the default behavior of several AS3 operators. Just don’t use it where you need high performance.
There’s a lot of code above. Have you spotted a bug? Have a suggestion to optimize either version? Did I miss something about Proxy
? By all means, post a comment about it!
#1 by J Murphy on February 27th, 2013 ·
This is exactly the information i was looking for. I’ve only worked with the Proxy class in other peoples code, and I’m working on defining a new Immutable Iterator Class that still allows for-each loops. This is a great starting point. Thanks for the article.