When a recent comment asked about string concatenation performance, I realized that there are a lot of ways to build strings in AS3 and I hadn’t tested any of them. Leaving aside the sillier ones like the XML class or joining Array objects, we have two serious contenders: the lowly + operator (i.e. str + str) and the ByteArray class. Which will triumph as the ultimate way to build strings quickly?

To test these two contenders I have devised a test application. One major factor in the performance here is how many objects are created and left as junk for the garbage collector to pick up. For more on this, see Hidden Object Allocations. To include this performance hit, a String is built every frame using a variety of methods:

  • += Literal: str += “abc”
  • += String: str += str
  • ByteArray UTF Literal: bytes.writeUTFBytes(“abc”)
  • ByteArray UTF Variable: bytes.writeUTFBytes(str”)
  • ByteArray ASCII Literal: bytes.writeMultiByte(“abc”, “us-ascii”)
  • ByteArray ASCII Literal: bytes.writeMultiByte(str, “us-ascii”)

Buttons are provided to switch between the various methods of building the string and the framerate is displayed. So, let’s take a look at the source code:

package
{
	import flash.display.DisplayObject;
	import flash.display.Sprite;
	import flash.display.StageAlign;
	import flash.display.StageScaleMode;
	import flash.events.Event;
	import flash.events.MouseEvent;
	import flash.text.TextField;
	import flash.text.TextFieldAutoSize;
	import flash.text.TextFormat;
	import flash.utils.ByteArray;
	import flash.utils.getTimer;
 
	public class StringBuildingSpeed extends Sprite
	{
		private static const PAD:Number = 5;
 
		private var stats:TextField = new TextField();
 
		private static const MODE_PLUS_LITERAL:int = 1;
		private static const MODE_PLUS_VARIABLE:int = 2;
		private static const MODE_BYTEARRAY_UTF_LITERAL:int = 3;
		private static const MODE_BYTEARRAY_UTF_VARIABLE:int = 4;
		private static const MODE_BYTEARRAY_ASCII_LITERAL:int = 5;
		private static const MODE_BYTEARRAY_ASCII_VARIABLE:int = 6;
		private var mode:int = MODE_PLUS_LITERAL;
 
		private var lastFrameTime:uint;
		private var frameCount:uint;
		private var lastStatsUpdateTime:uint;
 
		public function StringBuildingSpeed()
		{
			stage.align = StageAlign.TOP_LEFT;
			stage.scaleMode = StageScaleMode.NO_SCALE;
			stage.frameRate = 60;
 
			makeButtons("+= Literal", "+= Variable", "ByteArray UTF Literal", "ByteArray UTF Variable", "ByteArray ASCII Literal", "ByteArray ASCII Variable");
 
			stats.autoSize = TextFieldAutoSize.LEFT;
			stats.y = this.height + PAD;
			addChild(stats);
 
			addEventListener(Event.ENTER_FRAME, onEnterFrame);
			frameCount = 0;
			lastFrameTime = 0;
			lastStatsUpdateTime = getTimer();
		}
 
		private function makeButtons(...labels): void
		{
			var curX:Number = PAD;
			var y:Number = 0;
			for each (var label:String in labels)
			{
				var tf:TextField = new TextField();
				tf.mouseEnabled = false;
				tf.selectable = false;
				tf.defaultTextFormat = new TextFormat("_sans", 16, 0x0071BB);
				tf.autoSize = TextFieldAutoSize.LEFT;
				tf.text = label;
				tf.name = "lbl";
				tf.background = true;
				tf.backgroundColor = 0xffffff;
 
				var button:Sprite = new Sprite();
				button.buttonMode = true;
				button.graphics.beginFill(0xF5F5F5);
				button.graphics.drawRect(0, 0, tf.width+PAD, tf.height+PAD);
				button.graphics.endFill();
				button.graphics.lineStyle(1);
				button.graphics.drawRect(0, 0, tf.width+PAD, tf.height+PAD);
				button.addChild(tf);
				button.addEventListener(MouseEvent.CLICK, onButton);
				tf.x = PAD/2;
				tf.y = PAD/2;
 
				button.x = curX;
				button.y = y;
				addChild(button);
 
				curX += button.width + PAD;
			}
		}
 
		private function onButton(ev:MouseEvent): void
		{
			var tf:TextField = ev.target.getChildByName("lbl");
			var lbl:String = tf.text;
			switch (lbl)
			{
				case "+= Literal":
					mode = MODE_PLUS_LITERAL;
					break;
				case "+= Variable":
					mode = MODE_PLUS_VARIABLE;
					break;
				case "ByteArray UTF Literal":
					mode = MODE_BYTEARRAY_UTF_LITERAL;
					break;
				case "ByteArray UTF Variable":
					mode = MODE_BYTEARRAY_UTF_VARIABLE;
					break;
				case "ByteArray ASCII Literal":
					mode = MODE_BYTEARRAY_ASCII_LITERAL;
					break;
				case "ByteArray ASCII Variable":
					mode = MODE_BYTEARRAY_ASCII_VARIABLE;
					break;
			}
		}
 
		private function onEnterFrame(ev:Event): void
		{
			var REPS:int = 1000000;
			var i:int;
			var str:String = "";
			var aaaaaaaaaa:String = "aaaaaaaaaa";
			var ba:ByteArray;
			switch (mode)
			{
				case MODE_PLUS_LITERAL:
					for (i = 0; i < REPS; ++i)
					{
						str += "aaaaaaaaaa";
					}
					break;
				case MODE_PLUS_VARIABLE:
					for (i = 0; i < REPS; ++i)
					{
						str += aaaaaaaaaa;
					}
					break;
				case MODE_BYTEARRAY_UTF_LITERAL:
					ba = new ByteArray();
					ba.writeShort(REPS*10);
					for (i = 0; i < REPS; ++i)
					{
						ba.writeUTFBytes("aaaaaaaaaa");
					}
					ba.position = 0;
					str = ba.readUTF();
					break;
				case MODE_BYTEARRAY_UTF_VARIABLE:
					ba = new ByteArray();
					ba.writeShort(REPS*10);
					for (i = 0; i < REPS; ++i)
					{
						ba.writeUTFBytes(aaaaaaaaaa);
					}
					ba.position = 0;
					str = ba.readUTF();
					break;
				case MODE_BYTEARRAY_ASCII_LITERAL:
					ba = new ByteArray();
					for (i = 0; i < REPS; ++i)
					{
						ba.writeMultiByte("aaaaaaaaaa", "us-ascii");
					}
					ba.position = 0;
					str = ba.readMultiByte(ba.length, "us-ascii");
					break;
				case MODE_BYTEARRAY_ASCII_VARIABLE:
					ba = new ByteArray();
					for (i = 0; i < REPS; ++i)
					{
						ba.writeMultiByte(aaaaaaaaaa, "us-ascii");
					}
					ba.position = 0;
					str = ba.readMultiByte(ba.length, "us-ascii");
					break;
			}
 
			// Update stats display
			frameCount++;
			var now:int = getTimer();
			var dTime:int = now - lastFrameTime;
			var elapsed:int = now - lastStatsUpdateTime;
			if (elapsed > 1000)
			{
				var framerateValue:Number = 1000 / (elapsed / frameCount);
				stats.text = "FPS: " + framerateValue.toFixed(1);
				lastStatsUpdateTime = now;
				frameCount = 0;
			}
			lastFrameTime = now;
		}
	}
}

I ran this test on 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 11.1.102.63
  • 2.4 Ghz Intel Core i5
  • Mac OS X 10.7.3

And got these results: (higher numbers are faster)

Test FPS
+= Literal 6.1
+= Variable 6.1
ByteArray UTF Literal 7.5
ByteArray UTF Variable 7.5
ByteArray ASCII Literal 0.7
ByteArray ASCII Variable 0.7

Performance Chart

From these results we can draw some conclusions:

  • Building with variables is virtually exactly as fast as building with string literals
  • Writing UTF via ByteArray is the fastest approach
  • The + operator is 20% slower than writing UTF via ByteArray
  • Writing ASCII via ByteArray is vastly slower than either approach and should not be used when performance is desired

In most real-world cases, simply using the + operator will suffice. Surely, it will produce the cleanest code, be quickest to implement, and easiest to understand and maintain. However, if you really need to improve the speed at which you build strings then you should switch to using ByteArray and make absolutely certain that you use UTF and not ASCII. As mentioned last week, you can get further speedups by leaving the realm of pure AS3 and employing third party tools or languages to access the so-called Alchemy opcodes.

Spot a bug? Have a suggestion? Post a comment!