String.charCodeAt Is Really Fast
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; } } }
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 |
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; } } }
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 |
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!
#1 by henke37 on April 1st, 2013 ·
The reminds me of TextField.setTextFormat. I think it is an O(n*n) function for some reason.
#2 by Amit Patel on April 1st, 2013 ·
How … unexpected! Thanks for figuring this out :)
#3 by Boris Evieux on May 2nd, 2013 ·
I get opposite results when lauching your test here, could this be a hardware-dependent optimization?
Function, Time
private, 5919
charCodeAt(0), 10338
charCodeAt(20), 10338
Environment :
fp 11,6,602,171,
intel Core2 Quad Q6600 @2.39GHz
Win 7 64 pro.
While I’m here I want to thank you for all your work on this site. It is a very inspiring source when I seek optimization of my code.
#4 by jackson on May 2nd, 2013 ·
I’m glad you’re enjoying the site. Stay tuned for plenty of more articles on AS3 optimization. :)
As to your results, it definitely could be hardware-dependent. The CPU from your test is a few generations behind the one from my test, which could explain the difference. Also, are you sure you used a release version of the Flash Player?