(This does not benefit standard VM exports or HTML5/JS exports but will make them perform slower)
Recently, I stumbled across a nifty optimisation for GameMaker’s YYC (C++) compiler. In my tweening engine, I had noticed that some “simpler” easing scripts were performing much slower than more complicated ones.
For example…
/// EaseLinear()
return argument2 * argument0 / argument3 + argument1;
…was performing much slower than…
/// EaseInOutQuad()
var _arg0 = argument0/(argument3 * 0.5);
if (_arg0 < 1){ return argument2 * 0.5 * _arg0 * _arg0 + argument1; }
return argument2 * -0.5 * (--_arg0 * (_arg0 - 2) - 1) + argument1;
This had me baffled. So, I tweaked various parameters, attempting to find what was driving the non-sensical performance difference. Eventually, I noticed the unique thing EaseInOutQuad had which EaseLinear didn’t:
operations involving a numerical constant
What I learned is that, under the hood, GameMaker seems to handle variables as a general data type which can be safely passed around to various expressions, functions, and scripts. I assume that these general types will then fetch and return their actual data type when needed, which may be a real number, string, array, or some other type. Apparently, this can lead to extra overhead when checking for a variable’s data type at runtime.
Now, what does this have to do with numerical constants helping speed things up? Well!
When you write an operation explicitly involving a numerical constant, it can be assumed that other variables interacting with the numerical constant are (should be) real numbers, or at least know how to deal with them. In the right places, this can enable GameMaker to optimise things by directly accessing a variable’s real value instead of accessing its general “packaged” data type first.
For example, in the most simplest form:
x = a; // What is ‘a’?
x = 0+a; // Compiler can assume ‘a’ is a real number
In the example above, GameMaker will first access (a) as a general type before assigning its actual value to (x). With (0+a), however, the real value from (a) will be directly accessed since the numerical constant (0) makes it safe to assume the intention of the operation. This can lead to speed gains, even with the (possible) slight overhead from the “+0″ operation. Stripped down, the compiled C++ output would basically look like this (but much messier)…
x = a; // Access ‘a‘ and find which type to assign x
x = 0 + a.val; // Directly assign the real value of ‘a‘ to x
Despite appearing more complex, the second line is faster as its real value is being directly accessed by the C++ code. The operation involving a numerical constant allows this to occur.
Now! To get the most out of this technique, we need to utilize the Order of Precedence.
x = a + 0 + b * c; // We can do better!
In the above example, I have attempted to directly access the real values of all variables in the expression by adding (+ 0). However! Because (b * c) has a higher predence and will be executed first, b and c will fail to get the intended optimsation of having their values directly accessed. Instead, we need to wrap brackets around the first executed variable and add a constant zero to it.
x = a + (0+b) * c; // That’s better!
Now, (0 + b) will be executed first, with (b) directly accessing its value. Because (b) is now assumed to be a real value, (c) can also assume it is real. And because ( (0+b) * c ) is assumed to be real, (a) can ALSO assume it is real when it is accessed last. As a result, all values will be directly accessed by the C++ code:
x = a.val + (0 + b.val) * c.val; // YAY!
Note that this trick also works when multiplying or dividing by constant values:
x = a + 0.5 * b * c; // Divide and conquer!
Note that using this technique directly with script/function parameters can sometimes do more harm than good. Script and function parameters may require general data types to be passed as arguments. Remember that the general type is safer to pass around?
x = AddValues(a+0, b+0, c+0); // Probably BAD
x = ShowNumber (a + (0+b) / c); // Probably GOOD
In the first example above, because of how the YYC works, the numerical constant (+0) would force all 3 parameter values to first be pre-calculated and assigned to 3 temporary “general” variables. This creates extra overhead and can slow things down. However, with the second example, the optimsation benefits for the single argument would likely speed things up, as the single parameter involves a more complex calculation, allowing direct access to ‘a’, ‘b’, and ‘c’ in a single expression. The cost of the “extra overhead” would likely be outweighed.
In regards to the easing algorithms I had mentioned at the start, placing ( 0 + argument0) at the start of EaseLinear was all that was needed to greatly boost its speed!
return (0+argument2) * argument0 / argument3 + argument1; // Huzzah!
Anyhow! There’s no sure way to know how this could help speed up your own code until you try. Check to see where it helps and where it doesn’t. Experiment and benchmark the results!
Also, be sure to check out the outputed C++ code for your project in the Asset Cache Directory. You can find the directory by going to File -> Preferences.