Inlining Math Functions
As a followup to my article on Inlining Math.ceil(), I decided to inline some more functions in the Math class. Read on for the code as well as tests proving correctness and speed.
First of all, I didn’t make inlined versions of any of the more complex functions in Math: acos, asin, atan, atan2, cos, exp, log, pow, random, round, sin, sqrt, or tan. It seems unlikely that an AS3 solution could beat these as the non-trivial amount of AS3 to implement them would likely perform slower than their native code equivalents. With that said, I did make AS3 versions of abs, ceil, floor, max, and min. Additionally, I made a pair of extra functions– max2 and min2– because var args is so slow. Here they are along with the correctness and speed tests:
package { import flash.display.*; import flash.utils.*; import flash.text.*; /** * An app with inlined versions of functions from the Math class as well * as tests proving their correctness and speed. * @author Jackson Dunstan */ public class InlinedMathTest extends Sprite { public function InlinedMathTest() { stage.align = StageAlign.TOP_LEFT; stage.scaleMode = StageScaleMode.NO_SCALE; var logger:TextField = new TextField(); logger.autoSize = TextFieldAutoSize.LEFT; addChild(logger); function log(msg:*): void { logger.appendText(msg + "\n"); } var tests:Array = [ ["abs", [-2.1], [-2], [0], [2], [2.1], [NaN] ], ["ceil", [-2.1], [-2], [0], [2], [2.1], [NaN] ], ["floor", [-2.1], [-2], [0], [2], [2.1], [NaN] ], ["max", [-2,2], [2,-1], [2,2], [0,0], [NaN,-1], [-1,NaN], [NaN,NaN], [2,3], [-2,-3], [2,3,4,5,6], [6,5,4,3,2], [1,2,3,NaN,4], [1,2,3,4,NaN] ], ["min", [-2,2], [2,-1], [2,2], [0,0], [NaN,-1], [-1,NaN], [NaN,NaN], [2,3], [-2,-3], [2,3,4,5,6], [6,5,4,3,2], [1,2,3,NaN,4], [1,2,3,4,NaN] ] ]; for each (var test:Array in tests) { var testName:String = test.shift(); log(testName + ":"); for each (var values:Array in test) { var resI:Number = this[testName].apply(this, values); var resM:Number = Math[testName].apply(Math, values); var same:Boolean = isNaN(resI) && isNaN(resM) || resI == resM; log("\t" + (same ? "PASS" : "FAIL") + ". Inline returned: " + resI + ". Math returned: " + resM + ". Input: " + values); } } const NUM_ITERATIONS:int = 1000000; var i:int; var beforeTime:int; var result:Number; var val:Number = 1; var val1:Number = 1; var val2:Number = 2; var val3:Number = 3; var val4:Number = 4; beforeTime = getTimer(); for (i = 0; i < NUM_ITERATIONS; ++i) { result = abs(val); } log("abs (inline): " + (getTimer()-beforeTime)); beforeTime = getTimer(); for (i = 0; i < NUM_ITERATIONS; ++i) { result = Math.abs(val); } log("abs (Math): " + (getTimer()-beforeTime)); log(""); beforeTime = getTimer(); for (i = 0; i < NUM_ITERATIONS; ++i) { result = ceil(val); } log("ceil (inline): " + (getTimer()-beforeTime)); beforeTime = getTimer(); for (i = 0; i < NUM_ITERATIONS; ++i) { result = Math.ceil(val); } log("ceil (Math): " + (getTimer()-beforeTime)); log(""); beforeTime = getTimer(); for (i = 0; i < NUM_ITERATIONS; ++i) { result = floor(val) } log("floor (inline): " + (getTimer()-beforeTime)); beforeTime = getTimer(); for (i = 0; i < NUM_ITERATIONS; ++i) { result = Math.floor(val); } log("floor (Math): " + (getTimer()-beforeTime)); log(""); beforeTime = getTimer(); for (i = 0; i < NUM_ITERATIONS; ++i) { result = max(val1, val2, val3, val4); } log("max (inline): " + (getTimer()-beforeTime)); beforeTime = getTimer(); for (i = 0; i < NUM_ITERATIONS; ++i) { result = Math.max(val1, val2, val3, val4); } log("max (Math): " + (getTimer()-beforeTime)); log(""); beforeTime = getTimer(); for (i = 0; i < NUM_ITERATIONS; ++i) { result = min(val1, val2, val3, val4); } log("min (inline): " + (getTimer()-beforeTime)); beforeTime = getTimer(); for (i = 0; i < NUM_ITERATIONS; ++i) { result = Math.min(val1, val2, val3, val4); } log("min (Math): " + (getTimer()-beforeTime)); log(""); beforeTime = getTimer(); for (i = 0; i < NUM_ITERATIONS; ++i) { result = max2(val1, val2); } log("max2 (inline): " + (getTimer()-beforeTime)); beforeTime = getTimer(); for (i = 0; i < NUM_ITERATIONS; ++i) { result = Math.max(val1, val2); } log("max2 (Math): " + (getTimer()-beforeTime)); log(""); beforeTime = getTimer(); for (i = 0; i < NUM_ITERATIONS; ++i) { result = min2(val1, val2); } log("min2 (inline): " + (getTimer()-beforeTime)); beforeTime = getTimer(); for (i = 0; i < NUM_ITERATIONS; ++i) { result = Math.min(val1, val2); } log("min2 (Math): " + (getTimer()-beforeTime)); log(""); } public function abs(val:Number): Number { return val < 0 ? -val : val; } public function ceil(val:Number): Number { return (!(val <= 0) && !(val > 0)) ? NaN : val == int(val) ? val : val >= 0 ? int(val+1) : int(val); } public function floor(val:Number): Number { return (!(val <= 0) && !(val > 0)) ? NaN : val == int(val) ? val : val < 0 ? int(val-1) : int(val); } public function max(val1:Number, val2:Number, ...rest): Number { if ((!(val1 <= 0) && !(val1 > 0)) || (!(val2 <= 0) && !(val2 > 0))) { return NaN; } var ret:Number = val1 > val2 ? val1 : val2; var numRest:int = rest.length; var cur:Number; for (var i:int = 0; i < numRest; ++i) { cur = rest[i]; if ((!(cur <= 0) && !(cur > 0))) { return NaN; } if (cur > ret) { ret = cur; } } return ret; } public function min(val1:Number, val2:Number, ...rest): Number { if ((!(val1 <= 0) && !(val1 > 0)) || (!(val2 <= 0) && !(val2 > 0))) { return NaN; } var ret:Number = val1 < val2 ? val1 : val2; var numRest:int = rest.length; var cur:Number; for (var i:int = 0; i < numRest; ++i) { cur = rest[i]; if ((!(cur <= 0) && !(cur > 0))) { return NaN; } if (cur < ret) { ret = cur; } } return ret; } public function max2(val1:Number, val2:Number): Number { if ((!(val1 <= 0) && !(val1 > 0)) || (!(val2 <= 0) && !(val2 > 0))) { return NaN; } return val1 > val2 ? val1 : val2; } public function min2(val1:Number, val2:Number): Number { if ((!(val1 <= 0) && !(val1 > 0)) || (!(val2 <= 0) && !(val2 > 0))) { return NaN; } return val1 < val2 ? val1 : val2; } } }
If the TextField output grows beyond where you can see all the text, simply right-click it, choose “Select All”, and copy/paste the output into a text editor. All of the correctness tests pass, so I won’t bother to post a long list of “PASS” lines. The speed tests are quite interesting though:
Function | 2.2 Ghz Intel Core 2 Duo, 2 GB RAM, Mac OS X 10.6 |
---|---|
abs | 22 inline, 65 Math |
ceil | 24 inline, 72 Math |
floor | 24 inline, 69 Math |
max | 759 inline, 212 Math |
min | 750 inline, 215 Math |
max2 | 23 inline, 75 Math |
min2 | 21 inline, 69 Math |
You may notice a curious bit of code repeated in many of the functions above:
(!(val <= 0) && !(val > 0))
For details on this, see Wednesday’s article on how to make a Faster isNaN() function. Also, make sure to note that the inline version of max and min are actually a good deal slower than their equivalents in Math. Do not accidentally use these in a fit of inlining everything! Instead, use the more common min2 and max2 and then simply use Math.min and Math.max where you need to pass more than two parameters.
Lastly, it is certainly possible to get even more speed out of these “inlined” functions by truly inlining them. You may have to do this manually by essentially replacing the function call with the body of the function itself. This will save you the function call time and cost you readability, maintainability, and all the other advantages that drive us to create functions in the first place. But if you really need every last drop of performance, by all means go for it.
#1 by Og2t on November 16th, 2009 ·
Very interesting Jackson, I am thinking of replacing Math class with JMath now :) Would you think the static method call will add a lot of microtime to the results? I think you’ve done some tests for that before?
#2 by jackson on November 16th, 2009 ·
You’re right that I’ve already tested that in my article on function performance. The static calls would add some time to it. If you were going to use a lot of math functions all in one place you might use non-static versions like this:
The cost of the local variable and the static instance getter are easily offset by the large number of static math calls you’d save. It’s pretty clean too.