Conditionals Performance Revisited
Today I’m revisiting an article I wrote last August about conditionals: if-else
chains, ternary (? :
) operators, and switch
statements. In that article I showed that if-else
chains are about as fast as ternary operators and that both of them are 10-15% faster than switch
statements. Today we’ll take a look at how those conditionals scale beyond just the few cases in the last article.
To test how each approach (if-else
, ternary, switch
) scales, I began with the test application from the previous article and made a few modifications:
- Increased the number of cases from 5 (0..4) to 20 (0..19). Almost all conditionals have fewer than 20 cases.
- Based on the findings in Activation Objects, I moved the
log
log function out of the test function and made it aprivate
field - Switched ternary style based on a comment by whitered. This was very helpful as the nesting got deeper.
- Removed a lot of curly braces from the
if-else
chain to save room without sacrificing readability
Let’s see how the performance test looks now that those modifications have been applied:
package { import flash.text.*; import flash.utils.*; import flash.display.*; public class ConditionalsTestFollowup extends Sprite { private var __logger:TextField = new TextField(); private function log(msg:*): void { __logger.appendText(msg+"\n"); } public function ConditionalsTestFollowup() { stage.scaleMode = StageScaleMode.NO_SCALE; stage.align = StageAlign.TOP_LEFT; __logger.autoSize = TextFieldAutoSize.LEFT; addChild(__logger); var beforeTime:int; var afterTime:int; var ifElseTime:int; var ternaryTime:int; var switchTime:int; var i:int; const ITERATIONS:int = 50000000; log("Iterations,If-Else,Ternary,Switch"); for (var val:int = 0; val < 20; ++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 if (val == 4) func4(); else if (val == 5) func5(); else if (val == 6) func6(); else if (val == 7) func7(); else if (val == 8) func8(); else if (val == 9) func9(); else if (val == 10) func10(); else if (val == 11) func11(); else if (val == 12) func12(); else if (val == 13) func13(); else if (val == 14) func14(); else if (val == 15) func15(); else if (val == 16) func16(); else if (val == 17) func17(); else if (val == 18) func18(); else func19(); } afterTime = getTimer(); ifElseTime = afterTime - beforeTime; beforeTime = getTimer(); for (i = 0; i < ITERATIONS; ++i) { val == 0 ? func0() : val == 1 ? func1() : val == 2 ? func2() : val == 3 ? func3() : val == 4 ? func4() : val == 5 ? func5() : val == 6 ? func6() : val == 7 ? func7() : val == 8 ? func8() : val == 9 ? func9() : val == 10 ? func10() : val == 11 ? func11() : val == 12 ? func12() : val == 13 ? func13() : val == 14 ? func14() : val == 15 ? func15() : val == 16 ? func16() : val == 17 ? func17() : val == 18 ? func18() : func19() } afterTime = getTimer(); ternaryTime = 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; case 4: func4(); break; case 5: func5(); break; case 6: func6(); break; case 7: func7(); break; case 8: func8(); break; case 9: func9(); break; case 10: func10(); break; case 11: func11(); break; case 12: func12(); break; case 13: func13(); break; case 14: func14(); break; case 15: func15(); break; case 16: func16(); break; case 17: func17(); break; case 18: func18(); break; default: func19(); } } afterTime = getTimer(); switchTime = afterTime - beforeTime; log(val + "," + ifElseTime + "," + ternaryTime + "," + switchTime); } } private function func0(): void{} private function func1(): void{} private function func2(): void{} private function func3(): void{} private function func4(): void{} private function func5(): void{} private function func6(): void{} private function func7(): void{} private function func8(): void{} private function func9(): void{} private function func10(): void{} private function func11(): void{} private function func12(): void{} private function func13(): void{} private function func14(): void{} private function func15(): void{} private function func16(): void{} private function func17(): void{} private function func18(): void{} private function func19(): void{} } }
I compiled this with MXMLC 4.1.0.16076 and ran it with the release version of the Flash Player 10.1.102.64 plugin. I then ran the test on a 2.4 Ghz Intel Core i5 with Mac OS X 10.6 and a 2.8 Ghz Intel Xeon W3530 with Windows 7. The result is a very large block of numbers that are hard to parse through, so here they are in graph form:
As you can see, the performance of all three conditionals scales linearly. This means that if-else
and the ternary operator continue to outperform switch
statements by 10-15% on both platforms regardless of how many cases there are or which case is actually matched. This is quite disappointing for switch
as it should beat its competition with larger numbers of cases, but sadly it is always the loser for reasons described in the previous article.
Bottom line: beware of switch
statements in performance-critical code!
#1 by Henke37 on January 4th, 2011 ·
You should compare with different statement structuring, such as a tree instead of a linear list.
Also, just for fun, compare how a lookup Vector with Function objects do.
#2 by jackson on January 4th, 2011 ·
The tests just test N compares, regardless of how you decide that you need to do the comparisons. Perhaps I’ll do a more general article on tree performance though…
As for comparing “lookup Vector” with “Function objects”, I’m not sure what you mean. Are you suggesting that I index a
Vector.<Function>
? I haven’t directly tested that, but I have tested Map Performance and Array vs. Vector. I imagine the performance would be terrible. :)Thanks for the suggestions!
#3 by skyboy on January 4th, 2011 ·
It would have faster access than if statements, particularly at deeper levels.
However, the function objects would massively slow it down, to the point of being ridiculous.
#4 by pleclech on January 4th, 2011 ·
Using Haxe or Apparat Asm expansion you can use the real lookup switch with no garbage code produce by the dumb compiler ;)
Of course the speed is gain only for values that are consecutives and ints.
Here an example using Apparat Asm :
beforeTime = getTimer();
for (i = 0; i < ITERATIONS; ++i) {
__asm(
__as3(val), // push val on the stack
LookupSwitch("default", "l0", "l1", "l2"….), // lookup switch will use the value on the stack to jump at label "l0" for value 0,
// l2 for value 2 and label "default" if nothing match
);
__asm("l0:"); // create a label for the switch
func0();
__asm(Jump("break")); // simulate break switch
__asm("l1:"); // create a label for the switch
func1();
__asm(Jump("break"));
__asm("l2:"); // create a label for the switch
func2();
__asm(Jump("break"));
//….
__asm("default:");
func19();
__asm("break:")
}
afterTime = getTimer();
realSwitchTime = afterTime – beforeTime;
#5 by jackson on January 4th, 2011 ·
Looks like a nice alternative! It’s a shame that
switch
is so badly implemented that it’s necessary to do this sort of thing. Perhaps a future version of the compiler will improve the bytecode generation forswitch
.#6 by skyboy on January 4th, 2011 ·
I don’t believe anyone at Adobe works on the compiler anymore, so it’s doubtful that this will happen before AS4 and the need for a new compiler. Alternatives are as good as you can get for now, unfortunately.
#7 by pleclech on January 4th, 2011 ·
The Flex team is working on a new compiler code name Falcon, but don’t know how will be the generated code (couldn’t be worse) ;) http://www.michelboudreau.com/2010/10/25/flex-roadmap-session/
#8 by jackson on January 4th, 2011 ·
Awesome. Maybe I’ll have another multi-part re-testing series of articles when it’s released. :)
#9 by pleclech on January 4th, 2011 ·
Here the result for the ‘real LookupSwitch’ as above with flash 10.2.151.49 :
Iterations,If-Else,Ternary,Switch,TableSwitch
0,502,621,808,400
1,412,409,543,374
2,438,388,654,399
3,396,379,581,376
4,415,587,673,402
5,502,538,733,403
6,932,709,795,376
7,568,564,792,377
8,609,609,888,438
9,616,664,905,363
10,738,793,1314,482
11,681,752,981,412
12,766,881,979,386
13,820,819,1028,393
14,865,932,1363,430
15,906,919,1141,367
16,937,955,1257,457
17,1021,1002,1919,394
18,1052,1116,1342,415
19,1057,1026,1432,445
#10 by jackson on January 4th, 2011 ·
The performance is a flat line, the way you would expect
switch
to work! You can see the other approaches scale up linearly as they are O(n), but the TableSwitch never takes more time as it is O(1). By the end of the 20-case chain, TableSwitch is 3x faster than regularswitch
!Thanks for posting your results! I can only hope that this optimization makes its way into a future MXMLC, or perhaps a helper tool like Apparat.
#11 by skyboy on January 4th, 2011 ·
It’s certainly faster for int/uint; However, there are still Objects that can be used, for which the pseudo-code I posted below would work best; By eliminating the overhead of the useless (at that point) lookupswitch statement. Adobe was headed in the right direction, but messed it up, which is too bad.
Also, a sidenote for Jackson: your reply comment box is fixed-width: you should change it to be a percentage, as replying gets irritating when 60% of the box is hidden from view.
#12 by jackson on January 5th, 2011 ·
Definitely. This is the reason that
switch
only supports constant integers (int
,char
, etc.) in languages like C/C++ and Java. What the compiler really should output is something like your version (like anif-else
chain) if the values are not constant integers and something like pleclech’s version if they are constant integers. Instead, they essentially output both, which is a total waste as shown by the 10-15% speed hit in the article.As for the comment box sizing, I think I’ve got it fixed now. Happy commenting! :)
#13 by skyboy on January 4th, 2011 ·
Going further than this: omit the lookupswitch statement altogether.
Structure would-be switch statements like this:
That should actually bring switch statements into the range of if statements, and still keep the break/no break functionality.
#14 by Paespedro on September 27th, 2011 ·
Thanks for sharing this. Got same results on my tests!
#15 by ryzed on March 17th, 2013 ·
Hi, maybe too late, but want ask about “nested ifs”.
I mean, something like this:
so, for 0…7 number of checks will == 3
for 0…31 number of checks will == 5
#16 by ryzed on March 17th, 2013 ·
Here some code: http://wonderfl.net/c/ffzvp
Used const ITERATIONS:int = 5000000; coz i have old laptop.
And results (last column – nested ifs):
Iterations,If-Else,Ternary,Switch
0,159,169,255,226
1,162,174,257,203
2,179,185,271,204
3,197,197,280,114
4,87,85,121,87
5,97,86,125,85
6,102,133,134,88
7,104,98,140,85
8,109,105,143,91
9,115,110,148,91
10,122,117,157,86
11,123,122,161,83
12,146,131,167,83
13,137,138,173,88
14,141,149,177,88
15,146,155,181,80
16,157,164,188,83
17,163,169,193,79
18,175,171,204,85
19,174,171,199,84
#17 by jackson on March 17th, 2013 ·
It’s an intriguing idea that was also mentioned in the comments for another article. I plan on doing a full test very soon. Thanks for the tip and the preliminary performance results!