Building Strings
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 += strByteArray 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 |
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 viaByteArray
- 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!
#1 by jonathan on April 2nd, 2012 ·
Hi,
you forget to test the “concat()” function : http://help.adobe.com/en_US/ActionScript/3.0_ProgrammingAS3/WS5b3ccc516d4fbf351e63e3d118a9b90204-7ef9.html
#2 by jackson on April 2nd, 2012 ·
I did some quick testing on this and found these results: (in FPS, higher is better)
concat() … 7
+= … 8.5
UTF … 10
ASCII … 2
#3 by ben w on April 2nd, 2012 ·
what would also make for interesting information here would be details from profiling the tests.
i.e. how many instances are created (and as such have to be garbage collected), this will mostly affect the += literal and += variable
#4 by jackson on April 2nd, 2012 ·
Each concatenation with the
+=
operator will result in a newly-createdString
. So in the above test the+=
versions are allocating 1000000String
objects. In contrast, theByteArray
versions are allocating aByteArray
object, a newString
when callingreadMultiByte
/readUTF
, and however many internal allocationsByteArray
is doing as it grows due towrite*
calls. To capture the performance hit of creating so many garbage objects, this test is done every frame and the FPS is counted.#5 by NemoStein on April 2nd, 2012 ·
I don’t get it… More is less, this time?
I mean, 7.5 what is faster than 0.7 what?
#6 by ben w on April 2nd, 2012 ·
I think it is in terms of Frames Per Second (FPS) rather than execution speed (time) despite some great efforts to throw us off the scent ;)
#7 by Henke37 on April 2nd, 2012 ·
How about the TextField.appendText method?
#8 by ben w on April 2nd, 2012 ·
he could add that in but I tried it and it starts to get slow fast…crashes the player at high numbers
#9 by Daniel on April 2nd, 2012 ·
Am I miss-reading the results? looks like it should be the other way around.
Writing UTF via ByteArray is the SLOWEST approach
The + operator is 20% FASTER than writing UTF via ByteArray
Writing ASCII via ByteArray is vastly FASTER than either approach and should not be used when performance is desired
I thought the time is for the time it takes to do 1 mil reps, so byteArray ASCII looks the fastest to me
#10 by Mem's on April 2nd, 2012 ·
It’s could be nice if you add on your charts the mention “Higher is better” and specify units (here is frames per seconds).
But it’s could be better to use instead of “frame per second for 1,000,000 op.”, use time measurement units like ns (nanoseconds) for average operation duration:
“+= Literal”: 6.1 fps -> “+= Literal”: ~6.1 ns
or op/s (operations per seconds) like http://jsperf.com does:
“+= Literal”: 6.1 fps -> “+= Literal”: ~6,100,000 op/s
#11 by Daniel on April 2nd, 2012 ·
oh, I get it… the
#12 by jackson on April 2nd, 2012 ·
There’s definitely been some confusion with the results in this article, so I’ll try to call out just what the performance results mean in future articles. For now I’ll just add a note to the article to clarify.
#13 by Daniel on April 3rd, 2012 ·
switching the title from Time to FPS helps. It makes sense to test across several frames as doing a one time loop doesn’t give us an idea of how long the garbage collection takes
#14 by AlexG on April 5th, 2012 ·
At first look I got wires crossed with FPS too. Usually it writes time data. But if you like FPS than use everywhere FPS instead of time. Though usually it writes time data in graphics. +1 point for creativity ! :)
#15 by skyboy on April 18th, 2012 ·
The + operator’s performance has increased substantially from older player versions; though ByteArray still wins out by handling its own garbage collection instead of letting it be passive. ByteArray also has the advantage that a separate length (perhaps
position
) variable can permit the creation of a single large ByteArray to be used for all String concatenation.Though, a run-time generated String (through ByteArray) will perform differently, but only in 1% of cases. Writing a full UTF 4-byte sequence for all characters regardless of length will result in a disastrous performance impact for certain operations.
#16 by Jeremy Rudd on February 2nd, 2013 ·
Can you add skyboy’s StringBuffer class to a revised String Concat perftest?
https://github.com/skyboy/AS3-Utilities/blob/master/skyboy/utils/StringBuffer.as
I’ve searched high and low on the net and can’t find such a comparison.
#17 by jackson on February 2nd, 2013 ·
Good idea. I’ll make a note for a followup article.
#18 by Jeremy Rudd on February 5th, 2013 ·
Maybe a good idea to include an inlined version of the StringBuffer (whatever code it uses) to get a maximal speed boost in your followup. (function calls are expensive)
AND if you can use an alchemy version, that simply writes ASCII bytes into an alchemy ByteArray that might be very very fast?
Obviously to work with UTF strings in alchemy without the ByteArray.X methods you would need an open source UTF library (pretty much rewriting the AS3 String class)