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";
}

Run the test

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

XMLDocument Performance Graph

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!