What other mathematical operators can we use to transform an algorithm


The difference operator, (similar to the derivative operator), and the sum operator, (similar to the integration operator), can be used to change an algorithm because they are inverses.

Sum of (difference of y) = y
Difference of (sum of y) = y

An example of using them that way in a c program is below.

This c program demonstrates three approaches to making an array of squares.

  1. The first approach is the simple obvious approach, y = x*x .
  2. The second approach uses the equation (difference in y) = (x0 + x1)*(difference in x) .
  3. The third approach is the reverse and uses the equation (sum of y) = x(x+1)(2x+1)/6 .

The second approach is consistently slightly faster then the first one, even though I haven't bothered optimizing it. I imagine that if I tried harder I could make it even better.

The third approach is consistently twice as slow, but this doesn't mean the basic idea is dumb. I could imagine that for some function other than y = x*x this approach might be faster. Also there is an integer overflow issue.

Trying out all these transformations was very interesting, so now I want to know what are some other pairs of mathematical operators I could use to transform the algorithm?

Here is the code:

#include <stdio.h>
#include <time.h>

#define tries 201
#define loops 100000

void printAllIn(unsigned int array[tries]){
unsigned int index;

for (index = 0; index < tries; ++index)
    printf("%u\n", array[index]);

int main (int argc, const char * argv[]) {

    Goal, Calculate an array of squares from 0 20 as fast as possible


    long unsigned int obvious[tries];
    long unsigned int sum_of_differences[tries];
    long unsigned int difference_of_sums[tries];

    clock_t time_of_obvious1;
    clock_t time_of_obvious0;

    clock_t time_of_sum_of_differences1;
    clock_t time_of_sum_of_differences0;

    clock_t time_of_difference_of_sums1;
    clock_t time_of_difference_of_sums0;

    long unsigned int j;
    long unsigned int index;
    long unsigned int sum1;
    long unsigned int sum0;
    long signed int signed_index;

    time_of_obvious0 = clock();
    for (j = 0; j < loops; ++j)
    for (index = 0; index < tries; ++index)
        obvious[index] = index*index;
    time_of_obvious1 = clock();

        time_of_sum_of_differences0 = clock();
    for (j = 0; j < loops; ++j)
    for (index = 1, sum_of_differences[0] = 0; index < tries; ++index)
        sum_of_differences[index] = sum_of_differences[index-1] + 2 * index - 1;
    time_of_sum_of_differences1 = clock();

    time_of_difference_of_sums0 = clock();
    for (j = 0; j < loops; ++j)
    for (signed_index = 0, sum0 = 0; signed_index < tries; ++signed_index) {
        sum1 = signed_index*(signed_index+1)*(2*signed_index+1);
        difference_of_sums[signed_index] = (sum1 - sum0)/6;
        sum0 = sum1;
    time_of_difference_of_sums1 = clock();

    // printAllIn(obvious);
       "The obvious approach y = x*x took, %f seconds\n",
       ((double)(time_of_obvious1 - time_of_obvious0))/CLOCKS_PER_SEC
    // printAllIn(sum_of_differences);
       "The sum of differences approach y1 = y0 + 2x - 1 took, %f seconds\n",
       ((double)(time_of_sum_of_differences1 - time_of_sum_of_differences0))/CLOCKS_PER_SEC
    // printAllIn(difference_of_sums);
       "The difference of sums approach y = sum1 - sum0, sum = (x - 1)x(2(x - 1) + 1)/6 took, %f seconds\n",
       (double)(time_of_difference_of_sums1 - time_of_difference_of_sums0)/CLOCKS_PER_SEC

    return 0;

There are two classes of optimizations here: strength reduction and peephole optimizations.

Strength reduction is the usual term for replacing "expensive" mathematical functions with cheaper functions -- say, replacing a multiplication with two logarithm table lookups, an addition, and then an inverse logarithm lookup to find the final result.

Peephole optimizations is the usual term for replacing something like multiplication by a power of two with left shifts. Some CPUs have simple instructions for these operations that run faster than generic integer multiplication for the specific case of multiplying by powers of two.

You can also perform optimizations of individual algorithms. You might write a * b, but there are many different ways to perform multiplication, and different algorithms perform better or worse under different conditions. Many of these decisions are made by the chip designers, but arbitrary-precision integer libraries make their own choices based on the merits of the primitives available to them.