Now that the Flash Player 10.1 testing is through I can return to a comment asking about the performance difference between if-else chains and the ternary (? :) operator. Further, I’ll discuss switch statements to see if there is any difference in performance for these commonly-used methods of flow control.

All AS3 programmers make tons of use of if-else chains and many also use switch statements and the ternary (? :) operator. Given their essential nature, it’s important to know the performance differences, if any, between them. It would seem that since none of these constructs exist at the bytecode level that all of them would be compiled down to the same bytecode using conditional jumps/branches. Consider the following very simple functions designed to be easily read in bytecode:

private function ifElse6(val:int): void
{
    if (val == 0)
    {
        func0();
    }
    else if (val == 1)
    {
        func1();
    }
    else if (val == 2)
    {
        func2();
    }
    else if (val == 3)
    {
        func3();
    }
    else
    {
        func4();
    }
}
 
private function ternary6(val:int): void
{
    val == 0
        ? func0()
        : val == 1
            ? func1()
            : val == 2
                ? func2()
                : val == 3
                    ? func3()
                    : func4();
}
 
private function switch6(val:int): void
{
    switch (val)
    {
        case 0: func0(); break;
        case 1: func1(); break;
        case 2: func2(); break;
        case 3: func3(); break;
        default: func4();
    }
}

Each of them is simply calling a function based on the value of val, be it 0, 1, 2, 3, or something else. The switch version is arguably the cleanest and the ternary arguably the hardest to read since it’s usually a bad idea to have such a deeply-nested ternary statement. The if-else version is somewhere in the middle as it is straightforward but verbose. Let’s start analyzing the generated bytecode with this if-else version:

  function private::ifElse6(int):void    /* disp_id 0*/
  {
    // local_count=2 max_scope=1 max_stack=2 code_len=67
    0       getlocal0         
    1       pushscope         
    2       getlocal1         
    3       pushbyte          0
    5       ifne              L1
 
    9       getlocal0         
    10      callpropvoid      private::func0 (0)
    13      jump              L2
 
    L1: 
    17      getlocal1         
    18      pushbyte          1
    20      ifne              L3
 
    24      getlocal0         
    25      callpropvoid      private::func1 (0)
    28      jump              L2
 
    L3: 
    32      getlocal1         
    33      pushbyte          2
    35      ifne              L4
 
    39      getlocal0         
    40      callpropvoid      private::func2 (0)
    43      jump              L2
 
    L4: 
    47      getlocal1         
    48      pushbyte          3
    50      ifne              L5
 
    54      getlocal0         
    55      callpropvoid      private::func3 (0)
    58      jump              L2
 
    L5: 
    62      getlocal0         
    63      callpropvoid      private::func4 (0)
 
    L2: 
    66      returnvoid        
  }

This bytecode is nearly as straightforward as the AS3 code it was compiled from. It’s simply as sequence of skipping over blocks of code, i.e., the function calls, where val doesn’t pass the equality test and then skipping to the end of the function after val does match.

Since the ternary version should theoretically just be syntax sugar, let’s see how MXMLC compiles it:

  function private::ternary6(int):void    /* disp_id 0*/
  {
    // local_count=2 max_scope=1 max_stack=2 code_len=71
    0       getlocal0         
    1       pushscope         
    2       getlocal1         
    3       pushbyte          0
    5       equals            
    6       iffalse           L1
 
    10      getlocal0         
    11      callpropvoid      private::func0 (0)
    14      jump              L2
 
    L1: 
    18      getlocal1         
    19      pushbyte          1
    21      equals            
    22      iffalse           L3
 
    26      getlocal0         
    27      callpropvoid      private::func1 (0)
    30      jump              L2
 
    L3: 
    34      getlocal1         
    35      pushbyte          2
    37      equals            
    38      iffalse           L4
 
    42      getlocal0         
    43      callpropvoid      private::func2 (0)
    46      jump              L2
 
    L4: 
    50      getlocal1         
    51      pushbyte          3
    53      equals            
    54      iffalse           L5
 
    58      getlocal0         
    59      callpropvoid      private::func3 (0)
    62      jump              L2
 
    L5: 
    66      getlocal0         
    67      callpropvoid      private::func4 (0)
 
    L2: 
    70      returnvoid        
  }

This version is very similar to the if-else version, but unfortunately involves more stack access as it keeps using equals then iffalse rather than directly using ifne. This would be like writing in AS3 if ((val == 3) == true) rather than the much more common if (val == 3). They both have the same effect, but one pointlessly uses more instructions.

The switch statement is quite fancy in AS3 compared to, for example, C/C++ or Java as it can work on non-integer types and, unlike C#, supports falling through even when there is code in a case. Let’s see how this translates into bytecode:

  function private::switch6(int):void    /* disp_id 0*/
  {
    // local_count=3 max_scope=1 max_stack=2 code_len=140
    0       getlocal0         
    1       pushscope         
    2       jump              L1
 
 
    L2: 
    6       label             
    7       getlocal0         
    8       callpropvoid      private::func0 (0)
    11      jump              L3
 
 
    L4: 
    15      label             
    16      getlocal0         
    17      callpropvoid      private::func1 (0)
    20      jump              L3
 
 
    L5: 
    24      label             
    25      getlocal0         
    26      callpropvoid      private::func2 (0)
    29      jump              L3
 
 
    L6: 
    33      label             
    34      getlocal0         
    35      callpropvoid      private::func3 (0)
    38      jump              L3
 
 
    L7: 
    42      label             
    43      getlocal0         
    44      callpropvoid      private::func4 (0)
    47      jump              L3
 
    L1: 
    51      getlocal1         
    52      setlocal2         
    53      pushbyte          0
    55      getlocal2         
    56      ifstrictne        L8
 
    60      pushbyte          0
    62      jump              L9
 
    L8: 
    66      pushbyte          1
    68      getlocal2         
    69      ifstrictne        L10
 
    73      pushbyte          1
    75      jump              L9
 
    L10: 
    79      pushbyte          2
    81      getlocal2         
    82      ifstrictne        L11
 
    86      pushbyte          2
    88      jump              L9
 
    L11: 
    92      pushbyte          3
    94      getlocal2         
    95      ifstrictne        L12
 
    99      pushbyte          3
    101     jump              L9
 
    L12: 
    105     jump              L13
 
    109     pushbyte          4
    111     jump              L9
 
    L13: 
    115     pushbyte          4
 
    L9: 
    117     kill              2
    119     lookupswitch      default:L7 maxcase:4 L2 L4 L5 L6 L7
 
    L3: 
    139     returnvoid        
  }

Well this version sure is different! Even though it has the same effect as the other two versions, MXMLC produces bytecode that’s about twice as long. and makes use of some special instructions like lookupswitch. It starts off by jumping past all of the case blocks and then using yet-another approach to jump/branch logic compared to the if-else and ternary versions. Here, similar to the if-else version, ifstrictne is used rather than the boolean test that the ternary version used. Still, it’s using ifstrictne instead of the plain ifne version, which is analogous to using the AS3 !== operator instead of !=. This isn’t necessary though since the values being compared are simply of int type, but we’ll have to wait and see if it contributes to any performance degradation. Regardless, this jumping/branching doesn’t actually jump into the case blocks, but rather sets up the arguments to the lookupswitch instruction which does the actual work to decide which case statement should be executed.

So how do all of the above differences manifest themselves in actual performance? We’ll let’s look at a quick performance test:

package
{
    import flash.text.*;
    import flash.utils.*;
    import flash.display.*;
 
    public class ConditionalsTest extends Sprite
    {
        public function ConditionalsTest()
        {
            stage.scaleMode = StageScaleMode.NO_SCALE;
            stage.align = StageAlign.TOP_LEFT;
            var logger:TextField = new TextField();
            logger.autoSize = TextFieldAutoSize.LEFT;
            addChild(logger);
            function log(msg:*): void { logger.appendText(msg+"\n"); }
 
            var beforeTime:int;
            var afterTime:int;
            var i:int;
            const ITERATIONS:int = 50000000;
 
            for each (var val:int in [0,1,2,3,4])
            {
                log(val);
                beforeTime = getTimer();
                for (i = 0; i < ITERATIONS; ++i)
                {
                    if (val == 0)
                    {
                        func0();
                    }
                    else if (val == 1)
                    {
                        func1();
                    }
                    else if (val == 2)
                    {
                        func2();
                    }
                    else if (val == 3)
                    {
                        func3();
                    }
                    else
                    {
                        func4();
                    }
                }
                afterTime = getTimer();
                log("\tIf-else: " + (afterTime-beforeTime));
 
                beforeTime = getTimer();
                for (i = 0; i < ITERATIONS; ++i)
                {
                    val == 0
                        ? func0()
                        : val == 1
                            ? func1()
                            : val == 2
                                ? func2()
                                : val == 3
                                    ? func3()
                                    : func4();
                }
                afterTime = getTimer();
                log("\tTernary: " + (afterTime-beforeTime));
 
                beforeTime = getTimer();
                for (i = 0; i < ITERATIONS; ++i)
                {
                    switch (val)
                    {
                        case 0: func0(); break;
                        case 1: func1(); break;
                        case 2: func2(); break;
                        case 3: func3(); break;
                        default: func4();
                    }
                }
                afterTime = getTimer();
                log("\tSwitch: " + (afterTime-beforeTime));
            }
        }
 
        private function func0(): void{}
        private function func1(): void{}
        private function func2(): void{}
        private function func3(): void{}
        private function func4(): void{}
    }
}

This test is designed to show the differences between hitting on the first attempt (val == 0), the second, third, fourth, and default cases. Here are the results:

Environment If-Else Ternary Switch
0 1 2 3 4 0 1 2 3 4 0 1 2 3 4
3.0 Ghz Intel Core 2 Duo, Windows XP 361 400 453 457 455 360 405 437 457 456 443 468 504 526 502
2.0 Ghz Intel Core 2 Duo, Mac OS X 702 737 750 892 858 702 737 753 892 859 725 802 874 902 927

Here are some observations:

  • Aside from overall speed, there seem to be no performance differences between the operating systems involved.
  • We do see a general slowdown as val increased and it look more comparisons to find its match, regardless of the type of conditional used. It would have been nice if switch could have improved this, but this wasn’t expected from the bytecode above.
  • The if-else and ternary versions are nearly identical from a performance standpoint. It seems as though the boolean test we saw in the bytecode doesn’t have much of any impact, especially on Mac OS X.
  • Using switch is about 15% slower on Windows XP and 10% slower on Mac OS X. Beware of switches in performance-critical code!