XMLDocument: The Faster Legacy XML Option
Sometimes the old, legacy option is faster than the new one you’re supposed to use. That happens to be the case with XML in Flash: XMLDocument
is quicker than XML
. Today’s article tests its performance to figure out just how much faster it is and if it can keep up with plain Object
and typed class
instances.
XMLDocument is a port of the AS2 XML
class that Adobe kept around to make porting AS2 to AS3 easier. They also introduced the XML
class as the new class that you’re supposed to write new code against. That’s the class that supports E4X operators (e.g. xml.@x
) and has generally expanded functionality, like functions to find elements and descendants by name.
That last bit can be particularly annoying. With XML
you can get all the elements named x
by simply using E4X (xml.x
) or the class’ methods (xml.elements("x")
). With XMLDocument
you need to write a loop. Here’s my inline function for ASC 2.0:
[Inline] static private function getChildNodes( parent:XMLNode, name:String, into:Vector.<XMLNode> ): void { for each (var childNode:XMLNode in parent.childNodes) { if (childNode.nodeName == name) { into.push(childNode); } } }
Use it like this:
function foo(doc:XMLDocument): void { var found:Vector.<XMLNode> = new <XMLNode>[]; getChildNodes(doc, "x", found); // "found" now has a list of the elements that were found }
The reason for the Vector
argument is to allow you to re-use a Vector
instead of allocating a new one each time. This optimization is possible since we’re writing our own version of XML.elements()
and can therefore be a little more flexible. Similarly, it’d be easy to write a version that stops after it finds the first matching element. That’s functionality that doesn’t exist in the XML
class’ built-in methods.
Getting descendants is even more painful. With XML
you can use E4X (xml..x
) or the class method (xml.descendants("x")
). XMLDocument
forces us to write our own search function. Here’s mine:
[Inline] static private function getDescendantNodes( parent:XMLNode, name:String, into:Vector.<XMLNode> ): void { var stack:Vector.<XMLNode> = new <XMLNode>[parent]; for (var stackLen:uint = 1; stackLen > 0; ) { stackLen--; var children:Array = stack.pop().childNodes; for each (var childNode:XMLNode in children) { if (childNode.hasChildNodes()) { stack.push(childNode); stackLen++; } if (childNode.nodeName == name) { into.push(childNode); } } } }
Again, it’d be easy to write a version that stops at the first match, stops at a certain depth, or searches breadth-first rather than depth-first. That’s not possible with the built-in functionality of XML
.
Now to test the speed of XMLDocument
directly against XML
:
package { import flash.display.*; import flash.utils.*; import flash.text.*; import flash.xml.*; public class XMLDocumentTest extends Sprite { private var logger:TextField = new TextField(); private function row(...cols): void { logger.appendText(cols.join(",") + "\n"); } private static var XML_OBJ:XML; private static var XML_DOC:XMLDocument; private static var XML_DOC_ROOT:XMLNode; private static var PLAIN_OBJ:Object; private static var TYPED_OBJ:TypedObj; public function XMLDocumentTest() { stage.align = StageAlign.TOP_LEFT; stage.scaleMode = StageScaleMode.NO_SCALE; logger.autoSize = TextFieldAutoSize.LEFT; addChild(logger); XML_OBJ = <a b="x"><d><f/></d></a>; PLAIN_OBJ = { b:"x" }; TYPED_OBJ = new TypedObj(); XML_DOC = new XMLDocument(); XML_DOC.ignoreWhite = true; XML_DOC.parseXML(XML_OBJ.toXMLString()); XML_DOC_ROOT = XML_DOC.firstChild; init(); } private function init(): void { const REPS:int = 1000000; var i:int; var beforeTime:int; var afterTime:int; var childNode:XMLNode; var found:Vector.<XMLNode> = new <XMLNode>[]; var attrExistsTime:int; var attrDoesNotExistTime:int; var elemExistsTime:int; var elemDoesNotExistTime:int; var descExistsTime:int; var descDoesNotExistTime:int; row( "System", "Attribute Exists", "Attribute Does Not Exist", "Element Exists", "Element Does Not Exist", "Descendant Exists", "Descendant Does Not Exist" ); beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { XML_OBJ.@b; } afterTime = getTimer(); attrExistsTime = afterTime - beforeTime; beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { XML_OBJ.@c; } afterTime = getTimer(); attrDoesNotExistTime = afterTime - beforeTime; beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { XML_OBJ.d; } afterTime = getTimer(); elemExistsTime = afterTime - beforeTime; beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { XML_OBJ.e; } afterTime = getTimer(); elemDoesNotExistTime = afterTime - beforeTime; beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { XML_OBJ..f; } afterTime = getTimer(); descExistsTime = afterTime - beforeTime; beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { XML_OBJ..g; } afterTime = getTimer(); descDoesNotExistTime = afterTime - beforeTime; row( "E4X", attrExistsTime, attrDoesNotExistTime, elemExistsTime, elemDoesNotExistTime, descExistsTime, descDoesNotExistTime ); beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { XML_OBJ.attribute("b"); } afterTime = getTimer(); attrExistsTime = afterTime - beforeTime; beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { XML_OBJ.attribute("c"); } afterTime = getTimer(); attrDoesNotExistTime = afterTime - beforeTime; beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { XML_OBJ.elements("d"); } afterTime = getTimer(); elemExistsTime = afterTime - beforeTime; beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { XML_OBJ.elements("e"); } afterTime = getTimer(); elemDoesNotExistTime = afterTime - beforeTime; beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { XML_OBJ.descendants("f"); } afterTime = getTimer(); descExistsTime = afterTime - beforeTime; beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { XML_OBJ.descendants("g"); } afterTime = getTimer(); descDoesNotExistTime = afterTime - beforeTime; row( "XML Class Methods", attrExistsTime, attrDoesNotExistTime, elemExistsTime, elemDoesNotExistTime, descExistsTime, descDoesNotExistTime ); beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { XML_DOC_ROOT.attributes.b; } afterTime = getTimer(); attrExistsTime = afterTime - beforeTime; beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { XML_DOC_ROOT.attributes.c; } afterTime = getTimer(); attrDoesNotExistTime = afterTime - beforeTime; beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { found.length = 0; getChildNodes(XML_DOC_ROOT, "d", found); } afterTime = getTimer(); elemExistsTime = afterTime - beforeTime; beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { found.length = 0; getChildNodes(XML_DOC_ROOT, "e", found); } afterTime = getTimer(); elemDoesNotExistTime = afterTime - beforeTime; beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { found.length = 0; getDescendantNodes(XML_DOC_ROOT, "f", found); } afterTime = getTimer(); descExistsTime = afterTime - beforeTime; beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { found.length = 0; getDescendantNodes(XML_DOC_ROOT, "g", found); } afterTime = getTimer(); descDoesNotExistTime = afterTime - beforeTime; row( "XMLDocument", attrExistsTime, attrDoesNotExistTime, elemExistsTime, elemDoesNotExistTime, descExistsTime, descDoesNotExistTime ); beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { XML_OBJ["d"]; } afterTime = getTimer(); elemExistsTime = afterTime - beforeTime; beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { XML_OBJ["e"]; } afterTime = getTimer(); elemDoesNotExistTime = afterTime - beforeTime; row( "XML (bracket)", 0, 0, elemExistsTime, elemDoesNotExistTime, 0, 0 ); beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { PLAIN_OBJ.b; } afterTime = getTimer(); elemExistsTime = afterTime - beforeTime; beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { PLAIN_OBJ.c; } afterTime = getTimer(); elemDoesNotExistTime = afterTime - beforeTime; row( "Plain Object", 0, 0, elemExistsTime, elemDoesNotExistTime, 0, 0 ); beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { TYPED_OBJ.b; } afterTime = getTimer(); elemExistsTime = afterTime - beforeTime; row( "Typed Object", attrExistsTime, 0, 0, 0, 0, 0 ); } [Inline] static private function getChildNodes( parent:XMLNode, name:String, into:Vector.<XMLNode> ): void { for each (var childNode:XMLNode in parent.childNodes) { if (childNode.nodeName == name) { into.push(childNode); } } } [Inline] static private function getDescendantNodes( parent:XMLNode, name:String, into:Vector.<XMLNode> ): void { var stack:Vector.<XMLNode> = new <XMLNode>[parent]; for (var stackLen:uint = 1; stackLen > 0; ) { stackLen--; var children:Array = stack.pop().childNodes; for each (var childNode:XMLNode in children) { if (childNode.hasChildNodes()) { stack.push(childNode); stackLen++; } if (childNode.nodeName == name) { into.push(childNode); } } } } } } class TypedObj { public var b:String = "x"; }
I ran this test in the following environment:
- Release version of Flash Player 12.0.0.44
- 2.3 Ghz Intel Core i7-3615QM
- Mac OS X 10.9.1
- Google Chrome 32.0.1700.107
- ASC 2.0.0 build 354071 (
-debug=false -verbose-stacktraces=false -inline -optimize=true
)
And here are the results I got:
System | Attribute Exists | Attribute Does Not Exist | Element Exists | Element Does Not Exist | Descendant Exists | Descendant Does Not Exist |
---|---|---|---|---|---|---|
E4X | 341 | 304 | 356 | 308 | 1173 | 1066 |
XML Class Methods | 686 | 666 | 268 | 259 | 1060 | 1017 |
XMLDocument | 49 | 66 | 289 | 228 | 951 | 902 |
XML (bracket) | 0 | 0 | 393 | 357 | 0 | 0 |
Plain Object | 0 | 0 | 31 | 48 | 0 | 0 |
Typed Object | 49 | 0 | 0 | 0 | 0 | 0 |
Getting attributes of an XMLDocument
is really fast since it’s already in an Object
that’s being used as a map/hash with the keys happening to be the attribute name. In this test, XMLDocument
is stomping XML
by a factor of 5-10x.
Elements, as pointed out above, rely on an AS3 helper function and therefore have no really fast map of elements to query or even a built-in function to call. Even still, XMLDocument
is about as fast as XML
. If you just need the first element, it’d probably be even quicker.
When it comes to descendants, XMLDocument
still manages to hold its own and even beats out XML
by a small margin of about 10%. Again, specialized versions of the AS3 helper function are possible if you want to search breadth-first, for example.
Plain Object
and class
instances are included in the test as a means of comparison with “normal” objects. They continue to show far better performance than any XML
approach, but their lead is only marginal when it comes to accessing the attributes of an XMLDocument
. Still, they don’t possess nearly as much versatility as the XML classes as shown by the many tests they simply have no equivalent for. For its intended purpose—loading and parsing external data—the XML
and XMLDocument
classes should suffice, but you should get them parsed into typed class
instances before using them in performance-critical code.
In conclusion, if you’re looking for a way to speed up your XML-handling code, you don’t mind using legacy code, and you are willing to write some helper functions yourself, you can benefit from XMLDocument
with a performance boost ranging from about 10% to 10x.
Spot a bug? Have a suggestion or question? Post a comment!
#1 by Tyler on February 17th, 2014 ·
It seems like it might be fair to say that if you wanted to parse an XML string into a dynamic or typed object, XMLDocument would probably be significantly faster and not much more difficult to do that with.