Optimizing AS3 with JavaScript
Now that AS3 is performing slower than JavaScript in some areas, should we be looking to optimize our AS3 by offloading tasks to JavaScript? That may sound perverse, but the possibility of major speedups is tempting. Today’s article looks for speedups using Flash’s AS3-to-JavaScript bridge: ExternalInterface
.
Let’s address the elephants in the room first. For starters your SWF needs to be running in a browser so that there is a JavaScript environment to offload tasks to with ExternalInterface
. There’s also a marshalling cost to transfer parameters to JavaScript function from AS3 and to transfer return values from JavaScript functions to AS3. This and the overhead of the calls themselves are quite expensive, so you really need to minimize the amount of data you transfer between the two languages and the number of JavaScript calls you make.
The following performance test is based on the first sub-test of the AS3 vs. JavaScript Performance series where an Array
is joined into a String
many times. As of last week, JavaScript took 3 milliseconds to do that task while AS3 took 100. So there’s a good reason to offload this task to JavaScript if possible. The test is modified to concatenate all of the joins into one very long String
.
The JavaScript version of the test has been modified to take the Array
, perform the join operations, and return the length of the joined String
. There is a rather large marshalling cost to pass the Array
to JavaScript but the return value marshalling cost is low since it is simply a number. Since the JavaScript function does all 500 joins in one function call rather than calling JavaScript 500 times, the overhead of calling the JavaScript function is reduced. Hopefully this is a decent simulation of a large offloaded task that would be typical for code hoping to use JavaScript as an optimization.
package { import flash.text.TextField; import flash.external.ExternalInterface; import flash.utils.getTimer; import flash.display.Sprite; public class ExternalInterfaceOptimization extends Sprite { public function ExternalInterfaceOptimization() { var SIZE:int = 1000; var REPS:int = 500; var beforeTime:int; var afterTime:int; var i:int; var tf:TextField = new TextField(); tf.width = stage.stageWidth; tf.height = stage.stageHeight; tf.text = "Language,Time\n"; addChild(tf); var array:Array = []; for (i = 0; i < SIZE; ++i) { array[i] = i + 'a'; } beforeTime = getTimer(); var asString:String = ""; for (i = 0; i < REPS; ++i) { asString += array.join('-'); } afterTime = getTimer(); tf.appendText("AS3," + (afterTime-beforeTime) + "\n"); beforeTime = getTimer(); var jsStringLen:int = ExternalInterface.call("doJoin", array, REPS); afterTime = getTimer(); tf.appendText("JavaScript," + (afterTime-beforeTime) + "\n"); tf.appendText("\nSame string length? " + (asString.length==jsStringLen)); } } }
And here’s the JavaScript version of the function from the containing HTML page:
function doJoin(array, loops) { var str = ""; for (var i = 0; i < loops; ++i) { str += array.join('-'); } return str.length; }
I ran this test in the following environment:
- Release version of Flash Player 11.7.700.169
- Google Chrome 26.0.1410.65
- 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:
Language | Time |
---|---|
AS3 | 112 |
JavaScript | 11 |
These results mostly reflect what we saw last week with the 100-to-3 advantage of JavaScript over AS3. The test environment is a little different, but it does the original test in 108 milliseconds for AS3 and 4 milliseconds for JavaScript. Since the new version concatenates the join
results we should expect it to take a little bit longer than the original. The JavaScript version should take a little bit longer too, but it now has all the ExternalInterface
overhead: parameter marshalling, call overhead, return value marshalling.
That overhead is significant, but the performance advantage of JavaScript in this case is so large that JavaScript is still ~10x faster overall. This is probably not a case that you’ll want to include in your own apps, but there are many other similar scenarios where the same technique applies. Could your app use a 10x speedup in some areas?
Lastly, if ExternalInterface
isn’t available or it happens to be slow you should probably have an AS3 version to fall back on. That’s as simple as an if
statement:
// Abstract how the joins happen in a wrapper function // Use [Inline] with ASC 2.0 for maximum performance function doJoin(arr:Array, reps:int): String { if (ExternalInterface.available) { return ExternalInterface.call("doJoin", arr, reps); } else { // AS3 version return result; } }
Spot a bug? Have a question or suggestion? Post a comment!
#1 by Wilson Silva on April 15th, 2013 ·
On this environment:
Release version of Flash Player 11.7.700.179
Google Chrome 26.0.1410.64 m
2.53 Ghz Intel Core 2 Duo
Windows 8 Pro x64
My test results were:
AS3,187
JavaScript,20
This is awesome because now I know I can speed things up, but it’s sad because I shouldn’t have to.
#2 by pleclech on April 15th, 2013 ·
If you return the string back from javascript and not the length, is performance still valuable ?
#3 by jackson on April 15th, 2013 ·
Yes, it was about 2x faster instead of 10x. This is due to the far greater marshalling cost of returning a huge
String
from JavaScript to AS3. A 2x speedup is nothing to sneeze at, but it’s starting to push the limits where marshalling cost makes any JavaScript speedup irrelevant. The key is to find certain kinds of functions that are faster in JavaScript, require minimal to modest marshalling, and don’t need to be called many times. The function from the article has what I’d call “modest” marshalling and yields a 10x speedup. This is the kind of result that makes programmers actually think about implementing such a crazy optimization. :)#4 by pleclech on April 15th, 2013 ·
Ok. Thanks for the answer.
#5 by creynders on April 15th, 2013 ·
I’m curious what the results are if you do this with inline JS.
In the very unlikely case you don’t know what I’m talking about, here’s my how to on SO:
http://stackoverflow.com/questions/1891008/how-can-i-include-javascript-dynamically-from-flash-using-actionscript/1914748#1914748
I wouldn’t be amazed if it’s slower though, but it has the huge benefit that you don’t depend on external code.
#6 by creynders on April 15th, 2013 ·
Sorry, forgot to add that obviously you still need to pass the Array instance as a parameter to the function even though you declare it inline.
#7 by jackson on April 15th, 2013 ·
That’s a tricky way to use JavaScript. It does two things trickily. First it uses the awesome XML support in AS3 to copy variables into the
XML
object that’s being defined inline via curly braces. That’s how the “parameters” are being “passed” to the JavaScript function: they’re being written out as text in theXML
object. SecondExternalInterface
is being used to execute a JavaScript expression instead of calling a JavaScript function. The expression creates a new function on the fly with the contents of theXML
object.This technique has the advantage of creating the function entirely within the AS3 code files, which some may view as a better way to organize the project. However, there are three distinct performance disadvantages. First there is the need to create a new function each time you call it. Second, all parameters are passed as text in the definition of the function. Third, numerous functions will be created and possibly never cleaned up as they are called repeatedly throughout the program.
Perhaps a middle-ground is to define the function in AS3 at app startup, but keep it using traditional parameters. Then you can call it the same way as in the article and not suffer any per-call performance loss. In that case, I’d expect performance to be identical to the performance in the article, but with all JavaScript defined in the AS3 (if you consider that a benefit).
#8 by creynders on April 15th, 2013 ·
Yeah obviously it should be altered a bit.
> First there is the need to create a new function each time you call it.
That’s not really necessary, in the example i wrote it was only to avoid function name clashes, but you could give it a semantic name and simply check whether the function is already declared (i.e. it has already been called once)
> Second, all parameters are passed as text in the definition of the function.
That’s what I meant with “you still need to pass the Array instance as a parameter to the function”. Injecting the Array inline would mean the array is already concatenated inside AS and all benefits are lost.
>Third, numerous functions will be created and possibly never cleaned up as they are called repeatedly throughout the program.
See #1
#9 by jackson on April 15th, 2013 ·
Oh, I didn’t see that it was you who wrote the original code. Sorry for trying to explain your own code to you. :) I think we basically agree on my “middle-ground” approach: create the function once in AS3 and then call it as in the article. That would be a good way to keep the JavaScript source inside AS3.
#10 by creynders on April 15th, 2013 ·
>Sorry for trying to explain your own code to you.
You did a really fine job though :)
#11 by This test is done wrong. on April 23rd, 2013 ·
The javascript is caching the array.join(‘-‘) result.
if you add:
You should see that we get nowhere near this good performance boost.
#12 by jackson on April 23rd, 2013 ·
That may be true. I just happened to choose the first test from the “AS3 vs. JavaScript” series. Regardless of how JavaScript achieves the performance boost, I still think the conclusion of the article stands: