Comparing apples to oranges, was: Speed of exponentiation operator

I recently created a new function to display some data on a chart, which seemed to be doing what I wanted, but was taking >3seconds between screen refreshes (Tools, Preferences, Miscellaneous, Display chart timing).

On looking at “Code Check & Profile”, the script was executing a number of nested functions many times - I changed the code so that those functions only executed once, and showed a big improvement, but still quite slow.

Looking at the execution time again, showed that “array operator ^” (exponentiation operator) was taking most of the processing time. So I designed the following experiment:

// 
		function mthExponent(inpArray, inpExponent) 
/*
Description: this function returns an array raised to the power of the exponent.

Parameters:
	inpArray: the "base"
	inpExponent: the "exponent"

Refer to Wikipedia for a more complete explanation of exponentiation.
*/
{	
	return inpArray ^ inpExponent ;
}



// 
		function mthPower(inpArray, inpExponent, inpUseDefault) 
/*
Description: this function returns an array raised to the power of the exponent.

Parameters:
	inpArray: the "base"
	inpExponent: the "exponent"
	inpUseDefault: use AB's default exponetiation operator

Refer to Wikipedia for a more complete explanation of exponentiation.
*/
{
	// Use the default operator, or the user has specified an more complex exponenet
	if (inpUseDefault OR (frac(inpExponent) > 0) OR (inpExponent < 0))
		// This code executes in 10.23 ms for 100 iterations
		result	= inpArray ^ inpExponent ;
	else
	// The user has specified a simple positive integer exponent
	{		// This code executes in 0.421 ms for 100 iterations
		result	= inpArray ;
		if (inpExponent > 1)
		{
			for (loop = 2; loop <= inpExponent; loop ++)
			{
				result = result * inpArray ;
			}
		}
	}
	
	return result ;
}


// Create some random data
baseData	= mtRandomA(123) ;

// How many times to do each test
cnsNumIters	= 100 ;

// Test: default exponentiation operator
for (loop = 0; loop < cnsNumIters; loop ++)
	testDefA	= baseData ^ 2 ;

// Test: default exponentiation operator via functions -> timing
for (loop = 0; loop < cnsNumIters; loop ++)
testExpA	= mthExponent(baseData, 2) ;


doDefault	= False ;
if (doDefault)
{
	// Test: default exponentiation operator via function -> timing
	for (loop = 0; loop < cnsNumIters; loop ++)
		testPowA	= mthPower(baseData, 2, True) ;		// Uses default exponentiation operator
}
else
{
	// Test: multiplicative exponentiation via function -> timing
	for (loop = 0; loop < cnsNumIters; loop ++)
		testPowB	= mthPower(baseData, 2, False) ;	// Uses multiplication
}


// Sameness?
isSame		= AlmostEqual(testDefA, testExpA) ;
if (doDefault)
	isSame		= AlmostEqual(testDefA, testPowA) ;
else
	isSame		= AlmostEqual(testDefA, testPowB) ;


// Some other test, stops debug mode from obliterating values in "Watch" pane.
zzzz		= 1 + 2 + 3 + 4 + 5 ;

, and here’s the profile for it:
image

Does anyone else have similar experience or an explanation?

1 Like

That is normal because you are missing some info regarding how math works on modern CPUs and what ^ operator does and because of that you comparing apples to oranges.

Obviously SINGLE multiplication (that is what you are actually doing - you test your code with SQUARE only) is faster than exponentiation operator that is calling powf() C-runtime function.

Single multiplication is just FPU native hardware instruction. It executes in ONE cycle on modern CPUs.
Exponentiation operator is not implemented in hardware, it is complex function because it works with FRACTIONAL powers too (so it is general and allows to calculate roots, etc). When you use exponentiation you are actually calling powf() function. It is comparable to exp(x) and in fact many times it is implemented using exp(x). Even sqrt() - square root is faster because it is natively implemented in hardware CPU.

square_root = x ^ (1/2); // yes exponentiation operator works with fractions!)
square_root_fast = sqrt( x ); // yet NATIVE hardware FPU can calculate it faster
cubic_root = x ^ (1/3); // yes exponentiation operator works with fractions!
// no faster alternative for cubic root

The advantage of sequential multiplication is obvious when you do just single multiply. But when exponent raises, exponentiation operator is much faster. Change your code to use exponent = 102 and you will see that your multiplication-based approach is 10x slower:

// THIS IS YOUR CODE - I just added exponent larger than 2
// 
		function mthExponent(inpArray, inpExponent) 
/*
Description: this function returns an array raised to the power of the exponent.

Parameters:
	inpArray: the "base"
	inpExponent: the "exponent"

Refer to Wikipedia for a more complete explanation of exponentiation.
*/
{	
	return inpArray ^ inpExponent ;
}



// 
		function mthPower(inpArray, inpExponent, inpUseDefault) 
/*
Description: this function returns an array raised to the power of the exponent.

Parameters:
	inpArray: the "base"
	inpExponent: the "exponent"
	inpUseDefault: use AB's default exponetiation operator

Refer to Wikipedia for a more complete explanation of exponentiation.
*/
{
	// Use the default operator, or the user has specified an more complex exponenet
	if (inpUseDefault OR (frac(inpExponent) > 0) OR (inpExponent < 0))
		// This code executes in 10.23 ms for 100 iterations
		result	= inpArray ^ inpExponent ;
	else
	// The user has specified a simple positive integer exponent
	{		// This code executes in 0.421 ms for 100 iterations
		result	= inpArray ;
		if (inpExponent > 1)
		{
			for (loop = 2; loop <= inpExponent; loop ++)
			{
				result = result * inpArray ;
			}
		}
	}
	
	return result ;
}


// Create some random data
baseData	= mtRandomA(123) ;

// How many times to do each test
cnsNumIters	= 100 ;
testDefA	= baseData;

exponent = 102;

// Test: default exponentiation operator via functions -> timing
for (loop = 0; loop < cnsNumIters; loop ++)
testExpA	= mthExponent(baseData, exponent) ;


doDefault	= False ;
if (doDefault)
{
	// Test: default exponentiation operator via function -> timing
	for (loop = 0; loop < cnsNumIters; loop ++)
		testPowA	= mthPower(baseData, exponent, True) ;		// Uses default exponentiation operator
}
else
{
	// Test: multiplicative exponentiation via function -> timing
	for (loop = 0; loop < cnsNumIters; loop ++)
		testPowB	= mthPower(baseData, exponent, False) ;	// Uses multiplication
}


// Some other test, stops debug mode from obliterating values in "Watch" pane.
zzzz		= 1 + 2 + 3 + 4 + 5 ;

For these obvious reasons, instead of raising to 2, you should always write:

y = x * x; // always the fastest way to square, this is programmers 101

But since you made me aware that non-programmers may not know this, I will either issue a warning if people try to use ^ 2 or AFL engine would replace such inefficient statement with multiplication (although I am not big fan of such "inner workings" because I prefer having single code path, not some special cases) or both.

6 Likes

Thank you @Tomasz for the explanation, it's much appreciated.

My formulae need integer powers up to ^6, and are sometimes called hundreds of times - in retrospect, my code only needs squarings, and not all of the other features of "^".

In my test code, I deliberately avoided the more complex forms of exponentiation, because mathematically it's not as simple as it first appears (as you've explained, also in Wikipedia and other maths articles on the subject), but, I should have used much higher exponents as you've illustrated.

This topic was automatically closed 100 days after the last reply. New replies are no longer allowed.