Indexing Fields Is Really Slow
One of the very nice features of AS3 (and AS2 and JavaScript for that matter) is that you can dynamically access the fields of any object. This leads to much more dynamic code since you no longer need to know what field to access at compile time. As we’ve often seen with other dynamic features, this can come at a steep cost in terms of performance. Today we’ll see just how slow accessing fields this way is to get a good idea of just how much performance we give up when using this feature.
By way of introduction, here are the two ways of accessing fields of objects in question:
function foo(p:Point): void { p.x; // "dot" operator method of reading fields p["x"]; // "index" operator method of reading fields p.x = 1; // "dot" operator method of writing fields p["x"] = 1; // "index" operator method of writing fields }
In the dot operator method, you must know what field to access at compile time. In contrast, the index operator method basically shuts down all error checking on the compiler and lets you index any object with any expression: a literal, a variable, or the result of a computation.
With that in mind, I’ve updated the performance-testing app from Dynamic Field Access to include index operator reads and writes and do more tests per loop to lessen the impact of loop overhead. Performance results and graphs follow this test code.
package { import flash.display.*; import flash.events.*; import flash.utils.*; import flash.text.*; import flash.geom.*; public class FieldAccessMethods extends Sprite { public static var VAL:Number=0; private var __logger:TextField = new TextField(); private function row(...vals): void { __logger.appendText(vals.join(",")+"\n"); } public function FieldAccessMethods() { __logger.autoSize = TextFieldAutoSize.LEFT; addChild(__logger); var i:int; const REPS:int = 1000000; var beforeTime:int; var afterTime:int; var readDotTime:int; var writeDotTime:int; var readIndexTime:int; var writeIndexTime:int; var p:Point = new Point(0,0); var mp:MyPoint = new MyPoint(); var md:MyDynamic = new MyDynamic(); var d:Dictionary = new Dictionary(); d.x=0; var a:Array = [0]; a.x=0; var c:Class = DynamicAccess; var dc:Class = MyDynamic; var o:Object = {x:0}; row("Type", "Read (dot)", "Write (dot)", "Read (index)", "Write (index)"); beforeTime = getTimer(); for (i=0; i < REPS; ++i) { p.x;p.x;p.x;p.x;p.x; p.x;p.x;p.x;p.x;p.x; } afterTime = getTimer(); readDotTime = afterTime - beforeTime; beforeTime = getTimer(); for (i=0; i < REPS; ++i) { p.x=0;p.x=0;p.x=0;p.x=0;p.x=0; p.x=0;p.x=0;p.x=0;p.x=0;p.x=0; } afterTime = getTimer(); writeDotTime = afterTime - beforeTime; beforeTime = getTimer(); for (i=0; i < REPS; ++i) { p["x"];p["x"];p["x"];p["x"];p["x"]; p["x"];p["x"];p["x"];p["x"];p["x"]; } afterTime = getTimer(); readIndexTime = afterTime - beforeTime; beforeTime = getTimer(); for (i=0; i < REPS; ++i) { p["x"]=0;p["x"]=0;p["x"]=0;p["x"]=0;p["x"]=0; p["x"]=0;p["x"]=0;p["x"]=0;p["x"]=0;p["x"]=0; } afterTime = getTimer(); writeIndexTime = afterTime - beforeTime; row("Point", readDotTime, writeDotTime, readIndexTime, writeIndexTime); beforeTime = getTimer(); for (i=0; i < REPS; ++i) { mp.x;mp.x;mp.x;mp.x;mp.x; mp.x;mp.x;mp.x;mp.x;mp.x; } afterTime = getTimer(); readDotTime = afterTime - beforeTime; beforeTime = getTimer(); for (i=0; i < REPS; ++i) { mp.x=0;mp.x=0;mp.x=0;mp.x=0;mp.x=0; mp.x=0;mp.x=0;mp.x=0;mp.x=0;mp.x=0; } afterTime = getTimer(); writeDotTime = afterTime - beforeTime; beforeTime = getTimer(); for (i=0; i < REPS; ++i) { mp["x"];mp["x"];mp["x"];mp["x"];mp["x"]; mp["x"];mp["x"];mp["x"];mp["x"];mp["x"]; } afterTime = getTimer(); readIndexTime = afterTime - beforeTime; beforeTime = getTimer(); for (i=0; i < REPS; ++i) { mp["x"]=0;mp["x"]=0;mp["x"]=0;mp["x"]=0;mp["x"]=0; mp["x"]=0;mp["x"]=0;mp["x"]=0;mp["x"]=0;mp["x"]=0; } afterTime = getTimer(); writeIndexTime = afterTime - beforeTime; row("MyPoint", readDotTime, writeDotTime, readIndexTime, writeIndexTime); beforeTime = getTimer(); for (i=0; i < REPS; ++i) { md.x;md.x;md.x;md.x;md.x; md.x;md.x;md.x;md.x;md.x; } afterTime = getTimer(); readDotTime = afterTime - beforeTime; beforeTime = getTimer(); for (i=0; i < REPS; ++i) { md.x=0;md.x=0;md.x=0;md.x=0;md.x=0; md.x=0;md.x=0;md.x=0;md.x=0;md.x=0; } afterTime = getTimer(); writeDotTime = afterTime - beforeTime; beforeTime = getTimer(); for (i=0; i < REPS; ++i) { md["x"];md["x"];md["x"];md["x"];md["x"]; md["x"];md["x"];md["x"];md["x"];md["x"]; } afterTime = getTimer(); readIndexTime = afterTime - beforeTime; beforeTime = getTimer(); for (i=0; i < REPS; ++i) { md["x"]=0;md["x"]=0;md["x"]=0;md["x"]=0;md["x"]=0; md["x"]=0;md["x"]=0;md["x"]=0;md["x"]=0;md["x"]=0; } afterTime = getTimer(); writeIndexTime = afterTime - beforeTime; row("MyDynamic (existing)", readDotTime, writeDotTime, readIndexTime, writeIndexTime); beforeTime = getTimer(); for (i=0; i < REPS; ++i) { md.z;md.z;md.z;md.z;md.z; md.z;md.z;md.z;md.z;md.z; } afterTime = getTimer(); readDotTime = afterTime - beforeTime; beforeTime = getTimer(); for (i=0; i < REPS; ++i) { md.z=0;md.z=0;md.z=0;md.z=0;md.z=0; md.z=0;md.z=0;md.z=0;md.z=0;md.z=0; } afterTime = getTimer(); writeDotTime = afterTime - beforeTime; beforeTime = getTimer(); for (i=0; i < REPS; ++i) { md["z"];md["z"];md["z"];md["z"];md["z"]; md["z"];md["z"];md["z"];md["z"];md["z"]; } afterTime = getTimer(); readIndexTime = afterTime - beforeTime; beforeTime = getTimer(); for (i=0; i < REPS; ++i) { md["z"]=0;md["z"]=0;md["z"]=0;md["z"]=0;md["z"]=0; md["z"]=0;md["z"]=0;md["z"]=0;md["z"]=0;md["z"]=0; } afterTime = getTimer(); writeIndexTime = afterTime - beforeTime; row("MyDynamic (added)", readDotTime, writeDotTime, readIndexTime, writeIndexTime); beforeTime = getTimer(); for (i=0; i < REPS; ++i) { d.x;d.x;d.x;d.x;d.x; d.x;d.x;d.x;d.x;d.x; } afterTime = getTimer(); readDotTime = afterTime - beforeTime; beforeTime = getTimer(); for (i=0; i < REPS; ++i) { d.x=0;d.x=0;d.x=0;d.x=0;d.x=0; d.x=0;d.x=0;d.x=0;d.x=0;d.x=0; } afterTime = getTimer(); writeDotTime = afterTime - beforeTime; beforeTime = getTimer(); for (i=0; i < REPS; ++i) { d["x"];d["x"];d["x"];d["x"];d["x"]; d["x"];d["x"];d["x"];d["x"];d["x"]; } afterTime = getTimer(); readIndexTime = afterTime - beforeTime; beforeTime = getTimer(); for (i=0; i < REPS; ++i) { d["x"]=0;d["x"]=0;d["x"]=0;d["x"]=0;d["x"]=0; d["x"]=0;d["x"]=0;d["x"]=0;d["x"]=0;d["x"]=0; } afterTime = getTimer(); writeIndexTime = afterTime - beforeTime; row("Dictionary", readDotTime, writeDotTime, readIndexTime, writeIndexTime); beforeTime = getTimer(); for (i=0; i < REPS; ++i) { a.x;a.x;a.x;a.x;a.x; a.x;a.x;a.x;a.x;a.x; } afterTime = getTimer(); readDotTime = afterTime - beforeTime; beforeTime = getTimer(); for (i=0; i < REPS; ++i) { a.x=0;a.x=0;a.x=0;a.x=0;a.x=0; a.x=0;a.x=0;a.x=0;a.x=0;a.x=0; } afterTime = getTimer(); writeDotTime = afterTime - beforeTime; beforeTime = getTimer(); for (i=0; i < REPS; ++i) { a["x"];a["x"];a["x"];a["x"];a["x"]; a["x"];a["x"];a["x"];a["x"];a["x"]; } afterTime = getTimer(); readIndexTime = afterTime - beforeTime; beforeTime = getTimer(); for (i=0; i < REPS; ++i) { a["x"]=0;a["x"]=0;a["x"]=0;a["x"]=0;a["x"]=0; a["x"]=0;a["x"]=0;a["x"]=0;a["x"]=0;a["x"]=0; } afterTime = getTimer(); writeIndexTime = afterTime - beforeTime; row("Array", readDotTime, writeDotTime, readIndexTime, writeIndexTime); beforeTime = getTimer(); for (i=0; i < REPS; ++i) { c.VAL;c.VAL;c.VAL;c.VAL;c.VAL; c.VAL;c.VAL;c.VAL;c.VAL;c.VAL; } afterTime = getTimer(); readDotTime = afterTime - beforeTime; beforeTime = getTimer(); for (i=0; i < REPS; ++i) { c.VAL=0;c.VAL=0;c.VAL=0;c.VAL=0;c.VAL=0; c.VAL=0;c.VAL=0;c.VAL=0;c.VAL=0;c.VAL=0; } afterTime = getTimer(); writeDotTime = afterTime - beforeTime; beforeTime = getTimer(); for (i=0; i < REPS; ++i) { c["VAL"];c["VAL"];c["VAL"];c["VAL"];c["VAL"]; c["VAL"];c["VAL"];c["VAL"];c["VAL"];c["VAL"]; } afterTime = getTimer(); readIndexTime = afterTime - beforeTime; beforeTime = getTimer(); for (i=0; i < REPS; ++i) { c["VAL"]=0;c["VAL"]=0;c["VAL"]=0;c["VAL"]=0;c["VAL"]=0; c["VAL"]=0;c["VAL"]=0;c["VAL"]=0;c["VAL"]=0;c["VAL"]=0; } afterTime = getTimer(); writeIndexTime = afterTime - beforeTime; row("Static Class", readDotTime, writeDotTime, readIndexTime, writeIndexTime); beforeTime = getTimer(); for (i=0; i < REPS; ++i) { dc.VAL;dc.VAL;dc.VAL;dc.VAL;dc.VAL; dc.VAL;dc.VAL;dc.VAL;dc.VAL;dc.VAL; } afterTime = getTimer(); readDotTime = afterTime - beforeTime; beforeTime = getTimer(); for (i=0; i < REPS; ++i) { dc.VAL=0;dc.VAL=0;dc.VAL=0;dc.VAL=0;dc.VAL=0; dc.VAL=0;dc.VAL=0;dc.VAL=0;dc.VAL=0;dc.VAL=0; } afterTime = getTimer(); writeDotTime = afterTime - beforeTime; beforeTime = getTimer(); for (i=0; i < REPS; ++i) { dc["VAL"];dc["VAL"];dc["VAL"];dc["VAL"];dc["VAL"]; dc["VAL"];dc["VAL"];dc["VAL"];dc["VAL"];dc["VAL"]; } afterTime = getTimer(); readIndexTime = afterTime - beforeTime; beforeTime = getTimer(); for (i=0; i < REPS; ++i) { dc["VAL"]=0;dc["VAL"]=0;dc["VAL"]=0;dc["VAL"]=0;dc["VAL"]=0; dc["VAL"]=0;dc["VAL"]=0;dc["VAL"]=0;dc["VAL"]=0;dc["VAL"]=0; } afterTime = getTimer(); writeIndexTime = afterTime - beforeTime; row("Dynamic Class", readDotTime, writeDotTime, readIndexTime, writeIndexTime); beforeTime = getTimer(); for (i=0; i < REPS; ++i) { o.x;o.x;o.x;o.x;o.x; o.x;o.x;o.x;o.x;o.x; } afterTime = getTimer(); readDotTime = afterTime - beforeTime; beforeTime = getTimer(); for (i=0; i < REPS; ++i) { o.x=0;o.x=0;o.x=0;o.x=0;o.x=0; o.x=0;o.x=0;o.x=0;o.x=0;o.x=0; } afterTime = getTimer(); writeDotTime = afterTime - beforeTime; beforeTime = getTimer(); for (i=0; i < REPS; ++i) { o["x"];o["x"];o["x"];o["x"];o["x"]; o["x"];o["x"];o["x"];o["x"];o["x"]; } afterTime = getTimer(); readDotTime = afterTime - beforeTime; beforeTime = getTimer(); for (i=0; i < REPS; ++i) { o["x"]=0;o["x"]=0;o["x"]=0;o["x"]=0;o["x"]=0; o["x"]=0;o["x"]=0;o["x"]=0;o["x"]=0;o["x"]=0; } afterTime = getTimer(); writeDotTime = afterTime - beforeTime; row("Object", readDotTime, writeDotTime, readIndexTime, writeIndexTime); } private function foo(): void {} } } class MyPoint { public var x:Number=0; public var y:Number=0; } dynamic class MyDynamic { public var x:Number=0; public var y:Number=0; public static var VAL:Number=0; }
Here is the test environment I ran the test app on:
- Flex SDK (MXMLC) 4.1.0.16076, compiling in release mode (no debugging or verbose stack traces)
- Release version of Flash Player 10.2.154.27
- 2.4 Ghz Intel Core i5
- Mac OS X 10.6.7
And here are the results I got:
Type | Read (dot) | Write (dot) | Read (index) | Write (index) |
---|---|---|---|---|
Point | 2 | 12 | 816 | 892 |
MyPoint | 2 | 9 | 812 | 890 |
MyDynamic (existing) | 3 | 9 | 813 | 892 |
MyDynamic (added) | 555 | 369 | 855 | 1021 |
Dictionary | 305 | 427 | 995 | 1168 |
Array | 404 | 510 | 1207 | 1380 |
Static Class | 143 | 103 | 841 | 898 |
Dynamic Class | 140 | 103 | 809 | 886 |
Object | 831 | 1040 | 809 | 886 |
Here are those same results in graph form:
For all of the types except Object
, the index operator is more expensive. Way more expensive. For the dynamic Array
and Dictionary
types whose access is already slow, the performance hit is “only” about 3x. For the fast-access types (static classes), the performance hit can be up to 400x! Now let’s look at just the dot operator test results:
Here we see that accessing static fields is tons faster than accessing dynamic fields. Keep in mind that the “slow” accesses here are already hugely faster than the same classes (except Object
) when accessed with the index operator. All this means is that static field access is comparatively very fast.
Now, let’s look at just the index operator test results:
What’s striking about this graph is how most of the various performance differences between the different object types have disappeared. Dynamic class fields, Dictionary
, and Array
are slower than everything else. Strangely, this even includes Object
, which seems remarkably indifferent to access methods.
As for reading versus writing fields, we see two completely different pictures depending on access method. Writing is always about 10% slower than reading when using the index operator. On the other hand, the dot operator yields inconsistent results. The dynamic Object
, Dictionary
, and Array
classes are consistent with the dynamic index operator in being slower to write. This is even the case for the static fields, except with dynamic class instances and Class
objects. Still, the difference seems to be plus or minus 20% at most in all cases.
In sum, do not use the index operator to access fields in performance critical code. An exception would be when dealing with plain Object
variables, but these are so slow that you’d best avoid them in the first place.
#1 by Henke37 on May 2nd, 2011 ·
It would be interesting to compare declared fields in a dynamic class vs non declared fields in a dynamic class. Also interesting would be sequential array indexes vs sparse array indexes.
#2 by jackson on May 2nd, 2011 ·
The
MyDynamic
tests are for declared fields (“existing”) and non-declared fields (“added”). I declared anx
field and added az
field.As for
Array
’s sparse and dense portions, I’ve covered that before as well as some more advanced topics like what it takes to join the dense and sparse portions, etc. In short, dense reads are about 3x faster than sparse ones.#3 by Philippe on May 2nd, 2011 ·
It’s curious that added fields of a dynamic class are faster than Object.
#4 by jackson on May 2nd, 2011 ·
It is, isn’t it? Perhaps there is extra type checking necessary since
Object
could be anything. For this and other reasons, I usually prefer creatingDictionary
objects when making maps, etc.Dictionary
’s performance is also on par with dynamic classes, so at least there’s no need to make a dummy dynamic class (e.g.MyObject
). :)#5 by Hyzhak on May 3rd, 2011 ·
According to your article, you maybe should use
#6 by jackson on May 3rd, 2011 ·
If you have to use the index operator, yes, but if you have the choice of using the dot operator,
Dictionary
is way faster. Check out the second graph for a comparison.#7 by Wilson Silva on May 2nd, 2011 ·
Good to know! How do you access an array using the dot syntax?
#8 by Wilson Silva on May 2nd, 2011 ·
Nevermind I just saw your code. I didn’t know we could do it.
#9 by Paul Robertson on May 10th, 2011 ·
You can use dot syntax for Array instances, but then you’re not really using it as an array — you’re just using it as a dynamic class that happens to be built-in instead of one that you created yourself.
Any dynamic properties you use on an Array (such as “x”) don’t count as elements in the array — they don’t change it’s length property, you can’t access them with pop()/shift(), etc.
In other words, there’s nothing array specific about using an Array in this way; (putting aside performance differences) it’s really just a coincidence that you’re using an Array instead of an Object or MyDynamic.
Most importantly, you can’t use dot syntax for the elements of an array (the numeric-indexed properties) because the syntax isn’t valid–that’s why you have to use myArray[0] (or myArray[“0”]) instead of “myArray.0”.
#10 by jackson on May 10th, 2011 ·
This is all true and well said. I would only add that there are good reasons to add on to an
Array
, but they’re more akin to the style of JavaScript and AS2. In AS3, it’s better to create a strongly-typed class. Here’s an example I just thought up: