String Conversion
It struck me recently that there are a lot of ways to convert variables of many types to a the String type. The ease of doing this is one of AS3’s strengths over languages where it’s error-prone, possibly insecure, and just plain difficult. The C language is the most obvious example of this and, since then, seemingly every language has enshrined string conversion in ways ranging from global String() functions (AS3) that take any variable to adding toString() to the base Object type (Java, AS3, others). AS3 seems to have chosen “all of the above” and there are now many ways to convert to a string. Below I’ll look at them from a performance standpoint and see if the everyday, run-of-the-mill boring string conversion can be improved by choosing one option over another.
Let’s review the various ways of converting to String. Let me know in the comments if I’ve missed any.
// All types String(x); // All types ""+x; // All types x.toString(); // int, uint, Number only x.toString(10); // Array, Vector only x.join(); // Array, Vector only s = ""; for (i = 0; i < len; ++i) { s += x[i]; }
That’s six ways of converting to String, so now let’s put them to the test:
package { import flash.display.*; import flash.events.Event; import flash.text.*; import flash.utils.*; public class StringConversionTest extends Sprite { private var logger:TextField = new TextField(); private function log(msg:*): void { logger.appendText(msg + "\n"); } public function StringConversionTest() { logger.autoSize = TextFieldAutoSize.LEFT; addChild(logger); addEventListener(Event.ENTER_FRAME, testInt); } private function testInt(ev:Event): void { var beforeTime:int; var afterTime:int; var i:int; const REPS:int = 1000000; const TEMP_INT:int = 44; log("int"); beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { String(TEMP_INT); } afterTime = getTimer(); log("\tString(): " + (afterTime-beforeTime)); beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { ""+TEMP_INT; } afterTime = getTimer(); log("\tConcatenate: " + (afterTime-beforeTime)); beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { TEMP_INT.toString(); } afterTime = getTimer(); log("\ttoString(): " + (afterTime-beforeTime)); beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { TEMP_INT.toString(10); } afterTime = getTimer(); log("\ttoString(10): " + (afterTime-beforeTime)); removeEventListener(Event.ENTER_FRAME, testInt); addEventListener(Event.ENTER_FRAME, testUInt); } private function testUInt(ev:Event): void { var beforeTime:int; var afterTime:int; var i:int; const REPS:int = 1000000; const TEMP_UINT:uint = 44; log("uint"); beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { String(TEMP_UINT); } afterTime = getTimer(); log("\tString(): " + (afterTime-beforeTime)); beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { ""+TEMP_UINT; } afterTime = getTimer(); log("\tConcatenate: " + (afterTime-beforeTime)); beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { TEMP_UINT.toString(); } afterTime = getTimer(); log("\ttoString(): " + (afterTime-beforeTime)); beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { TEMP_UINT.toString(10); } afterTime = getTimer(); log("\ttoString(10): " + (afterTime-beforeTime)); removeEventListener(Event.ENTER_FRAME, testUInt); addEventListener(Event.ENTER_FRAME, testNumber); } private function testNumber(ev:Event): void { var beforeTime:int; var afterTime:int; var i:int; const REPS:int = 1000000; const TEMP_NUMBER:Number = 44.0; log("Number"); beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { String(TEMP_NUMBER); } afterTime = getTimer(); log("\tString(): " + (afterTime-beforeTime)); beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { ""+TEMP_NUMBER; } afterTime = getTimer(); log("\tConcatenate: " + (afterTime-beforeTime)); beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { TEMP_NUMBER.toString(); } afterTime = getTimer(); log("\ttoString(): " + (afterTime-beforeTime)); beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { TEMP_NUMBER.toString(10); } afterTime = getTimer(); log("\ttoString(10): " + (afterTime-beforeTime)); removeEventListener(Event.ENTER_FRAME, testNumber); addEventListener(Event.ENTER_FRAME, testBoolean); } private function testBoolean(ev:Event): void { var beforeTime:int; var afterTime:int; var i:int; const REPS:int = 1000000; const TEMP_BOOLEAN:Boolean = true; log("Boolean"); beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { String(TEMP_BOOLEAN); } afterTime = getTimer(); log("\tString(): " + (afterTime-beforeTime)); beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { ""+TEMP_BOOLEAN; } afterTime = getTimer(); log("\tConcatenate: " + (afterTime-beforeTime)); beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { TEMP_BOOLEAN.toString(); } afterTime = getTimer(); log("\ttoString(): " + (afterTime-beforeTime)); removeEventListener(Event.ENTER_FRAME, testBoolean); addEventListener(Event.ENTER_FRAME, testArray); } private function testArray(ev:Event): void { var beforeTime:int; var afterTime:int; var i:int; var j:int; var len:int; var s:String; const REPS:int = 1000000; const TEMP_ARRAY:Array = [1,2,3,4,5,6,7,8,9,10]; log("Array"); beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { String(TEMP_ARRAY); } afterTime = getTimer(); log("\tString(): " + (afterTime-beforeTime)); beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { ""+TEMP_ARRAY; } afterTime = getTimer(); log("\tConcatenate: " + (afterTime-beforeTime)); beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { TEMP_ARRAY.toString(); } afterTime = getTimer(); log("\ttoString(): " + (afterTime-beforeTime)); beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { TEMP_ARRAY.join(); } afterTime = getTimer(); log("\tjoin(): " + (afterTime-beforeTime)); beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { s = ""; for (j = 0, len = TEMP_ARRAY.length; j < len; ++j) { s += TEMP_ARRAY[j]; } } afterTime = getTimer(); log("\tfor loop: " + (afterTime-beforeTime)); removeEventListener(Event.ENTER_FRAME, testArray); addEventListener(Event.ENTER_FRAME, testVector); } private function testVector(ev:Event): void { var beforeTime:int; var afterTime:int; var i:int; var j:int; var len:int; var s:String; const REPS:int = 1000000; const TEMP_VECTOR:Vector.<int> = Vector.<int>([1,2,3,4,5,6,7,8,9,10]); log("Vector"); beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { String(TEMP_VECTOR); } afterTime = getTimer(); log("\tString(): " + (afterTime-beforeTime)); beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { ""+TEMP_VECTOR; } afterTime = getTimer(); log("\tConcatenate: " + (afterTime-beforeTime)); beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { TEMP_VECTOR.toString(); } afterTime = getTimer(); log("\ttoString(): " + (afterTime-beforeTime)); beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { TEMP_VECTOR.join(); } afterTime = getTimer(); log("\tjoin(): " + (afterTime-beforeTime)); beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { s = ""; for (j = 0, len = TEMP_VECTOR.length; j < len; ++j) { s += TEMP_VECTOR[j]; } } afterTime = getTimer(); log("\tfor loop: " + (afterTime-beforeTime)); removeEventListener(Event.ENTER_FRAME, testVector); } } }
Here are the results for a 3.0 Ghz Intel Core 2 Duo on Windows XP:
Type | String() | Concat | toString() | toString(10) | join() | for loop |
---|---|---|---|---|---|---|
int | 2 | 262 | 328 | 328 | n/a | n/a |
uint | 2 | 262 | 330 | 330 | n/a | n/a |
Number | 2 | 282 | 323 | 324 | n/a | n/a |
Boolean | 2 | 2 | 9 | n/a | n/a | n/a |
Array | 5901 | 5886 | 5861 | n/a | 5507 | 3782 |
Vector (int) | 5364 | 5379 | 4975 | n/a | 5127 | 3834 |
And here are the results for a 2.0 Ghz Intel Core 2 Duo on Mac OS X 10.5:
Type | String() | Concat | toString() | toString(10) | join() | for loop |
---|---|---|---|---|---|---|
int | 3 | 465 | 562 | 562 | n/a | n/a |
uint | 3 | 475 | 560 | 563 | n/a | n/a |
Number | 7 | 499 | 533 | 537 | n/a | n/a |
Boolean | 3 | 3 | 13 | n/a | n/a | n/a |
Array | 10769 | 10765 | 10677 | n/a | 10342 | 7522 |
Vector (int) | 10828 | 9963 | 9548 | n/a | 9557 | 7478 |
There are a number of observations we can take away from these results:
- The global String() conversion function is by far the fastest way of converting int, uint, and Number. For those types, the other methods are all equally terrible.
- Concatentation (“”+x) and toString() do not have nearly the penalty with Boolean as they do with int, uint, and Number. Since Booleans can be only true or false, one would expect some decent optimization to take place. This seems to be the case with concatenation, but toString() is still 4x slower slower, probably due to the function call that has not been removed by the compiler.
- There is little difference between the “native” ways of converting Arrays and Vectors: String(), concatentation, toString(), and join(). Sadly, the non-native approach of using a simple for loop is decidedly faster by about 40%. AS3 is outperforming native code here by a rather large margin.
- The performance does not seem to differ between Windows and Mac OS X.
While it’s unlikely that your application will bottleneck on String conversion, you probably convert to String all the time. As a general practice of not pointlessly throwing away CPU cycles, it’s good to know which of the many options is the fastest so that you can use it routinely. While the extra typing required to make a for loop to convert an Array or Vector is more often a nuissance than a nourishment, simply using the String() conversion function instead of concatenation with an empty string or calling toString() ensures that your code will run at least 100x faster while only adding a couple of characters.
#1 by jonathan on April 30th, 2010 ·
From the doc toString() is a “prototype” function.
#2 by Karl Knocking on May 8th, 2010 ·
So what does Flash use when I write trace(myInt);?
#3 by jackson on May 8th, 2010 ·
trace() takes var args (… args), and the documentation says that it calls toString() on your arguments. I haven’t tested it, but it would seem that calling trace(String(myInt)) may have a performance advantage if the implementation of trace is smart enough to not call toString() on an argument that is already a String.