String.charCodeAt is a simple function so you might expect the function call overhead (huge in AS3) to making calling it frequently quite slow. You’d think that there’s no way an charCodeAt-using AS3 function could beat a built-in String function like indexOf. Would you be right? Today’s article examines this special function to see if we might defy conventional wisdom and achieve a performance boost.

This article is prompted by a comment claiming that String.charCodeAt is somehow receiving an optimization unlike other functions: it seems to have less function call overhead or possibly none at all. The best way to test this is to write a tiny test app that calls String.charCodeAt and an empty private function of the test app class:

package
{
	import flash.text.TextField;
	import flash.utils.getTimer;
	import flash.display.Sprite;
 
	public class CharCodeAt extends Sprite
	{
		public function CharCodeAt()
		{
			init();
		}
 
		private function init(): void
		{
			var beforeTime:int;
			var afterTime:int;
			var i:int;
			var resInt:int;
			var REPS:int = 100000000;
 
			var LEN:int = 1000;
			var str:String = "<";
			for (i = 0; i < LEN-2; ++i)
			{
				str += "0";
			}
			str += ">";
 
			var out:String = "Function,Time\n";
 
			beforeTime = getTimer();
			for (i = 0; i < REPS; ++i)
			{
				resInt = foo();
			}
			afterTime = getTimer();
			out += "private," + (afterTime-beforeTime) + "\n";
 
			beforeTime = getTimer();
			for (i = 0; i < REPS; ++i)
			{
				resInt = str.charCodeAt(0);
			}
			afterTime = getTimer();
			out += "charCodeAt(0)," + (afterTime-beforeTime) + "\n";
 
			var tf:TextField = new TextField();
			tf.width = stage.stageWidth;
			tf.height = stage.stageHeight;
			tf.text = out;
			addChild(tf);
		}
 
		private function foo(): int { return 0; }
	}
}

Launch the test

I ran this test in the following environment:

  • Release version of Flash Player 11.6.602.180
  • 2.3 Ghz Intel Core i7
  • Mac OS X 10.8.3
  • ASC 2.0 build 352231 (-debug=false -verbose-stacktraces=false -inline)

And here are the results I got:

Function Time
private 662
charCodeAt(0) 186
charCodeAt(20) 186

String.charCodeAt Performance Graph

While it’s true that the empty private function’s only work is to return zero, this does seem to be confirmation of the hypothesis: there is reduced (or zero) function call overhead for String.charCodeAt. Is this because the new ASC 2.0 compiler is doing a great job of optimizing the function call? Let’s find out by looking at the bytecode it produces. Here’s the loop for charCodeAt(0) (annotated by me):

69      pushbyte 0 // push 0
      70      setlocal 7 // set i to what was pushed (0)
      71      jump bb5 // go to for loop conditional (see third block below)
    bb4
      succs=[bb5]
      72      label // no-op
      73      getlocal3 // get str
      74      pushbyte 0 // push 0 (index to charCodeAt)
      75      callproperty // call charCodeAt
      76      convert_i // convert result to an int
      77      setlocal 4 // set result to resInt
      78      inclocal_i 7 // increment i
    bb5
      succs=[bb4,bb6]
      79      getlocal 7 // get i
      80      getlocal2 // get REPS
      81      iflt bb4 // if i < REPS go to loop body

Most of that is just to manage the for loop but you can clearly see the function call is still happening with line 75: callproperty. This means that the optimization is happening at runtime within the Flash Player. Either the JIT or the VM are treating this function differently than others.

Now let’s move on to see if we can put this discovery to practice by creating AS3 equivalents of String functions where we use charCodeAt to potentially gain a speed boost. There are far too many functions of String for this article, so let’s just start out with indexOf and lastIndexOf:

[Inline]
public static function indexOfCharCode(
	str:String,
	charCode:Number,
	startIndex:int,
	len:int=-1
): int
{
	// Get length if not passed
	if (len < 0) len = str.length-1;
 
	for (var i:int = 0; i < len; ++i)
	{
		if (str.charCodeAt(i) == charCode)
		{
			return i;
		}
	}
	return -1;
}
 
[Inline]
public static function lastIndexOfCharCode(
	str:String,
	charCode:Number,
	startIndex:int=0x7FFFFFFF,
	len:int=-1
): int
{
	// Get length-1 as len
	if (len < 0) len = str.length-1;
	else len--;
 
	// Cap startIndex to len
	if (startIndex > len) startIndex = len;
 
	for (var i:int = startIndex; i >= 0; --i)
	{
		if (str.charCodeAt(i) == charCode)
		{
			return i;
		}
	}
	return -1;
}

These are simple functions for anyone to implement. They simply loop from one end of the string to the other looking for a given character code. The rest is just a bit of error case handling. Note that these are different from String.indexOf because they only look for a single character code instead of a whole sub-string. So keep in mind that this function is therefore quite a bit simpler and should be more efficient when you see the performance results later. Here’s the full performance test:

package
{
	import flash.text.TextField;
	import flash.utils.getTimer;
	import flash.display.Sprite;
 
	public class StringVsCharCodeAt extends Sprite
	{
		public function StringVsCharCodeAt()
		{
			init();
		}
 
		private function init(): void
		{
			var beforeTime:int;
			var afterTime:int;
			var i:int;
			var resInt:int;
			var REPS:int = 10000000;
 
			var LEN:int = 10;
			var END:int = LEN-1;
			var str:String = "<";
			for (i = 0; i < LEN-2; ++i)
			{
				str += "0";
			}
			str += ">";
			var beginCode:Number = str.charCodeAt(0);
			var endCode:Number = str.charCodeAt(END);
 
			var out:String = "Function,Time\n";
 
			beforeTime = getTimer();
			for (i = 0; i < REPS; ++i)
			{
				resInt = str.indexOf("<");
			}
			afterTime = getTimer();
			out += "indexOf begin," + (afterTime-beforeTime) + "\n";
 
			beforeTime = getTimer();
			for (i = 0; i < REPS; ++i)
			{
				resInt = indexOfCharCode(str, beginCode, 0, LEN);
			}
			afterTime = getTimer();
			out += "indexOfCharCode begin," + (afterTime-beforeTime) + "\n";
 
			beforeTime = getTimer();
			for (i = 0; i < REPS; ++i)
			{
				resInt = str.indexOf(">");
			}
			afterTime = getTimer();
			out += "indexOf end," + (afterTime-beforeTime) + "\n";
 
			beforeTime = getTimer();
			for (i = 0; i < REPS; ++i)
			{
				resInt = indexOfCharCode(str, endCode, 0, LEN);
			}
			afterTime = getTimer();
			out += "indexOfCharCode end," + (afterTime-beforeTime) + "\n";
 
			beforeTime = getTimer();
			for (i = 0; i < REPS; ++i)
			{
				resInt = str.lastIndexOf("<");
			}
			afterTime = getTimer();
			out += "lastIndexOf begin," + (afterTime-beforeTime) + "\n";
 
			beforeTime = getTimer();
			for (i = 0; i < REPS; ++i)
			{
				resInt = lastIndexOfCharCode(str, beginCode, END, LEN);
			}
			afterTime = getTimer();
			out += "lastIndexOfCharCode begin," + (afterTime-beforeTime) + "\n";
 
			beforeTime = getTimer();
			for (i = 0; i < REPS; ++i)
			{
				resInt = str.lastIndexOf(">");
			}
			afterTime = getTimer();
			out += "lastIndexOf end," + (afterTime-beforeTime) + "\n";
 
			beforeTime = getTimer();
			for (i = 0; i < REPS; ++i)
			{
				resInt = lastIndexOfCharCode(str, endCode, END, LEN);
			}
			afterTime = getTimer();
			out += "lastIndexOfCharCode end," + (afterTime-beforeTime) + "\n";
 
			var tf:TextField = new TextField();
			tf.width = stage.stageWidth;
			tf.height = stage.stageHeight;
			tf.text = out;
			addChild(tf);
		}
 
		[Inline]
		public static function indexOfCharCode(
			str:String,
			charCode:Number,
			startIndex:int,
			len:int=-1
		): int
		{
			// Get length if not passed
			if (len < 0) len = str.length-1;
 
			for (var i:int = 0; i < len; ++i)
			{
				if (str.charCodeAt(i) == charCode)
				{
					return i;
				}
			}
			return -1;
		}
 
		[Inline]
		public static function lastIndexOfCharCode(
			str:String,
			charCode:Number,
			startIndex:int=0x7FFFFFFF,
			len:int=-1
		): int
		{
			// Get length-1 as len
			if (len < 0) len = str.length-1;
			else len--;
 
			// Cap startIndex to len
			if (startIndex > len) startIndex = len;
 
			for (var i:int = startIndex; i >= 0; --i)
			{
				if (str.charCodeAt(i) == charCode)
				{
					return i;
				}
			}
			return -1;
		}
	}
}

Launch the test app

I ran this in the same environment as above and got these results:

Function Time
indexOf begin 252
indexOfCharCode begin 96
indexOf end 301
indexOfCharCode end 627
lastIndexOf begin 294
lastIndexOfCharCode begin 822
lastIndexOf end 259
lastIndexOfCharCode end 98

String vs. charCodeAt

These results are pretty clear. The charCodeAt-based approaches are over twice as fast when the character code to find is right at the beginning of the search and over twice as slow when the character code to find is all the way at the end. This is based on the low (or zero) overhead of charCodeAt compared to indexOf and lastIndexOf. As the work done by the function increases this overhead becomes relatively less important and the native code implementation beats the AS3 version handily.

In some special cases like indexOf for just a character code that you expect to be right at the front of the String there are optimizations to be had. It’s also very nice to know that this function is fast for purposes of handling text-based file formats like JSON and XML. But while we’ve found that charCodeAt has much less overhead than other functions, it’s not enough to go through all the String functions and re-write them.

Know any other functions with reduced overhead? Spot a bug? Have a question or suggestion? Post a comment!