-2

How can I simplify the following function and the repeated calls to Math.pow()?

function x(a, b, c) {
    rv = Math.floor(a * Math.pow(2, b));

    for (i = 1; i < c; i++) {
        rv += Math.floor(a * Math.pow(1.2, b + i));
    }

    return rv;
}
Danziger
  • 19,628
  • 4
  • 53
  • 83

2 Answers2

1

Based on the tags in your question, I will assume you want to improve that code's performance.

Here are some techniques you can use, more or less sorted by the performance gains they would bring to your code, with the exception of the first one, but I encourage you to implement them one by one on your code and see how they affect its performance individually.

✏️ The basics:

First, as Sterling Archer has suggested in a comment, you should use local variables instead of globals, so add let in front of them.

The performance gains for doing this are probably neglectable, but that's not the only reason to change it: Using globals is considered a bad practice, as they pollute the global namespace and make your code harder to maintain: Why are global variables considered bad practice?

⛓️ Memoizing the exponential:

Instead of executing Math.pow(1.2, b + i) on every iteration you can calculate the current iteration value using the previous one, as a multiplication should be considerably faster than an exponentiation:

let exp = Math.pow(1.2, b);

for (let i = 1; i < c; ++i) {
    exp  *= 1.2;
    rv += Math.floor(a * exp);
}

This technique is called memoization.

Floor with bitwise NOT operator (~):

If a is positive and the values you are flooring are always < 2147483648, you could use a bitwise NOT (~) instead of Math.floor(), as you can see here:

console.log(`Math.floor(4.01)  =  ${ Math.floor(4.01) }`);
console.log(`Math.floor(4.99)  =  ${ Math.floor(4.99) }`);
console.log(`Math.floor(-4.01) = ${ Math.floor(-4.01) }`);
console.log(`Math.floor(-4.99) = ${ Math.floor(-4.99) }`);

console.log(`~~4.01  =  ${ ~~4.01 }`);
console.log(`~~4.99  =  ${ ~~4.99 }`);
console.log(`~~-4.01 = ${ ~~-4.01 }`);
console.log(`~~-4.99 = ${ ~~-4.99 }`);

console.log(`Math.floor(2147483647.99) = ${ Math.floor(2147483647.99) }`);
console.log(`Math.floor(2147483648.01) = ${ Math.floor(2147483648.01) }`);
console.log(`~~2147483647.99 = ${ ~~2147483647.99 }`);
console.log(`~~2147483648.01 = ${ ~~2147483648.01 }`);
.as-console-wrapper {
  max-height: 100vh !important;
}

However, if you try to floor a value >= 2147483648, ~~ will wrap and return an incorrect value, as bitwise operators work with 32-bit integers, so the maximum value you can safely use is 231-1, or 2147483647.

Floor with bitwise operators alternative:

In that case, you could use Math.trunc() instead of Math.floor(), which is a bit faster, as you can see here: https://jsperf.com/number-truncating-methods/1

Base 2 exponential with left shift operator:

Similarly, if b is that integer s.t. 1 <= b <= 30, you can use the left shift (<<) instead of the first Math.pow(2, b): 2 << (b - 1):

for (let i = 0; i <= 31; ++i) {
  console.log(`Math.pow(2, ${ i }) === 2 << (${ i } - 1)? ${ Math.pow(2, i) === 2 << (i - 1) ? 'Yes' : 'No'}.` );
}
.as-console-wrapper {
  max-height: 100vh !important;
}

➰ Use a while loop:

Have you noticed that after applying the memoization technique we are not using the i variable inside the loop anymore? You could now replace the for with a while, which isn't going to provide big gains, but it's still worth mentioning this option:

while (--c) {
    exp *= 1.2;
    rv += ~~(a * exp);
} 

Final result:

Altogether, your superfast code would look like this:

function original(a, b, c) {
  rv = Math.floor(a * Math.pow(2, b));

  for (i = 1; i < c; i++) {
    rv += Math.floor(a * Math.pow(1.2, b+i));
  }

  return rv;
}

function faster(a, b, c) {
  let rv = ~~(a * (2 << (b - 1)));
  let exp = Math.pow(1.2, b);
  
  while (--c) {
    exp *= 1.2;
    rv += ~~(a * exp);
  } 

  return rv;
}

const expected = original(2, 2, 113);
const actual = faster(2, 2, 113);
const ok = expected === actual ;


if (ok) {
  // BEFORE:
  
  const t0b = performance.now();
  
  for (let i = 0; i < 100000; ++i) {
    original(2, 2, 113);
  }
  
  const tb = performance.now() - t0b;
  
  // AFTER:
  
  const t0a = performance.now();
  
  for (let i = 0; i < 100000; ++i) {
    faster(2, 2, 113);
  }
  
  const ta = performance.now() - t0a;
  
  console.log(` BEFORE = ${ tb }s`);
  console.log(`  AFTER = ${ ta }s`);
  console.log(`SPEEDUP = ${ Math.round(100 * tb / ta) / 100 } = ${ Math.round((1 - ta / tb) * 10000) / 100 }% faster!`);
  
} else {
  console.log(`Faster version = ${ actual } does not return the same as before = ${ expected }`);
}
.as-console-wrapper {
  max-height: 100vh !important;
}

Loop unrolling:

It's worth mentioning that, because you are flooring the results of each operation, this technique is not going to speed up the code too much, so probably it's not worth it, considering how verbose the code becomes.

You can read more about loop unrolling here.

However, if you weren't flooring the results, you would be able to save many calculations:

function faster (a, b, c) {
    let rv = a * (2 << (b - 1));
    let exp = Math.pow(1.2, b);

    const r = c % 4;

    if (r === 0) {
        exp *= 1.728;
        rv += a * a * a * exp;
    } else if (r === 1) {
        c += 3;
    } else if (r === 2) {
        exp *= 1.2;
        rv += a * exp;
        c += 2;
    } else if (r === 3) {
        exp *= 1.44;
        rv += a * a * exp;
        c += 1;
    }

    a = Math.pow(a, 4);
    c /= 4;

    while (--c) {
        exp *= 2.0736;
        rv += a * exp;
    }

    return rv;
}

As you can see, the advantage there is that you would be able to combine the calculations of multiple iterations in a single one, instead of just duplicating them, as you would have to do in your original code. Just for demonstrational purposes:

function original(a, b, c) {
  rv = Math.floor(a * Math.pow(2, b));

  for (i = 1; i < c; i++) {
    rv += Math.floor(a * Math.pow(1.2, b+i));
  }

  return rv;
}

function faster(a, b, c) {
  let rv = ~~(a * (2 << (b - 1)));
  let exp = Math.pow(1.2, b);
  
  const r = c % 4;
  
  if (r === 0) {
    exp *= 1.2;
    rv += ~~(a * exp);
    exp *= 1.2;
    rv += ~~(a * exp);
    exp *= 1.2;
    rv += ~~(a * exp);
  } else if (r === 1) {
    c += 3;
  } else if (r === 2) {
    exp *= 1.2;
    rv += ~~(a * exp);
    c += 2;
  } else if (r === 3) {
    exp *= 1.2;
    rv += ~~(a * exp);
    exp *= 1.2;
    rv += ~~(a * exp);
    c += 1;
  }
  
  c /= 4;
  
  while (--c) {
    exp *= 1.2;
    rv += ~~(a * exp);
    exp *= 1.2;
    rv += ~~(a * exp);
    exp *= 1.2;
    rv += ~~(a * exp);
    exp *= 1.2;
    rv += ~~(a * exp);
  }

  return rv;
}

const expected = original(2, 2, 113);
const actual = faster(2, 2, 113);
const ok = expected === actual;

if (ok) {
  // BEFORE:
  
  const t0b = performance.now();
  
  for (let i = 0; i < 100000; ++i) {
    original(2, 2, 113);
  }
  
  const tb = performance.now() - t0b;
  
  // AFTER:
  
  const t0a = performance.now();
  
  for (let i = 0; i < 100000; ++i) {
    faster(2, 2, 113);
  }
  
  const ta = performance.now() - t0a;
  
  console.log(` BEFORE = ${ tb }s`);
  console.log(`  AFTER = ${ ta }s`);
  console.log(`SPEEDUP = ${ Math.round(100 * tb / ta) / 100 } = ${ Math.round((1 - ta / tb) * 10000) / 100 }% faster!`);
  
} else {
  console.log(`Faster version = ${ actual } does not return the same as before = ${ expected }`);
}
.as-console-wrapper {
  max-height: 100vh !important;
}
Nimantha
  • 6,405
  • 6
  • 28
  • 69
Danziger
  • 19,628
  • 4
  • 53
  • 83
  • 1
    Bitwise operators instead of `Math.floor` and the `while` loop are micro-optimisations that aren't worth it. – Bergi Jun 04 '18 at 21:23
  • @Bergi I totally agree with the `while` and probably with `Math.floor()` vs `Math.trunc()`, but as that operation is executed multiple times inside a loop, `~~` still provides a decent gain (`30%` faster) which, in my opinion, is totally worth it if that function is normally going to execute the loop enough times so that it makes a difference. – Danziger Jun 04 '18 at 21:51
0

If you are worried about computing all 1.2^(b+i) for large ranges, think that underlying layers may infer the result from previous one in compilers optimizations. However if you want to help him explicitely, you could do something like

function x (a, b, c) {
  var rv = Math.floor(a * Math.pow(2, b))
  var multiplier = Math.pow(1.2, b + 1)
  for (i = 1; i < c; i++) {
    rv += Math.floor(a * multiplier);
    multiplier *= 1.2
  }
  return rv;
}

just Maths.

lucchi
  • 1,006
  • 6
  • 9