Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the fastest way to calculate / create powers of ten?

Tags:

python

If as the input you provide the (integer) power, what is the fastest way to create the corresponding power of ten? Here are four alternatives I could come up with, and the fastest way seems to be using an f-string:

from functools import partial
from time import time
import numpy as np

def fstring(power):
    return float(f'1e{power}')

def asterisk(power):
    return 10**power

methods = {
    'fstring': fstring,
    'asterisk': asterisk,
    'pow': partial(pow, 10),
    'np.pow': partial(np.power, 10, dtype=float)
}

# "dtype=float" is necessary because otherwise it will raise: 
# ValueError: Integers to negative integer powers are not allowed.
# see https://stackoverflow.com/a/43287598/5472354
powers = [int(i) for i in np.arange(-10000, 10000)]
for name, method in methods.items():
    start = time()
    for i in powers:
        method(i)
    print(f'{name}: {time() - start}')

Results:

fstring: 0.008975982666015625
asterisk: 0.5190775394439697
pow: 0.4863283634185791
np.pow: 0.046906232833862305

I guess the f-string approach is the fastest because nothing is actually calculated, though it only works for integer powers of ten, whereas the other methods are more complicated operations that also work with any real number as the base and power. So is the f-string actually the best way to go about it?

like image 866
mapf Avatar asked Mar 02 '23 09:03

mapf


2 Answers

You're comparing apples to oranges here. 10 ** n computes an integer (when n is non-negative), whereas float(f'1e{n}') computes a floating-point number. Those won't take the same amount of time, but they solve different problems so it doesn't matter which one is faster.

But it's worse than that, because there is the overhead of calling a function, which is included in your timing for all of your alternatives, but only some of them actually involve calling a function. If you write 10 ** n then you aren't calling a function, but if you use partial(pow, 10) then you have to call it as a function to get a result. So you're not actually comparing the speed of 10 ** n fairly.

Instead of rolling your own timing code, use the timeit library, which is designed for doing this properly. The results are in seconds for 1,000,000 repetitions (by default), or equivalently they are the average time in microseconds for one repetiton.

Here's a comparison for computing integer powers of 10:

>>> from timeit import timeit
>>> timeit('10 ** n', setup='n = 500')
1.09881673199925
>>> timeit('pow(10, n)', setup='n = 500')
1.1821871869997267
>>> timeit('f(n)', setup='n = 500; from functools import partial; f = partial(pow, 10)')
1.1401332350014854

And here's a comparison for computing floating-point powers of 10: note that computing 10.0 ** 500 or 1e500 is pointless because the result is simply an OverflowError or inf.

>>> timeit('10.0 ** n', setup='n = 200')
0.12391662099980749
>>> timeit('pow(10.0, n)', setup='n = 200')
0.17336435099969094
>>> timeit('f(n)', setup='n = 200; from functools import partial; f = partial(pow, 10.0)')
0.18887039500077663
>>> timeit('float(f"1e{n}")', setup='n = 200')
0.44305286100097874
>>> timeit('np.power(10.0, n, dtype=float)', setup='n = 200; import numpy as np')
1.491982370000187
>>> timeit('f(n)', setup='n = 200; from functools import partial; import numpy as np; f = partial(np.power, 10.0, dtype=float)')
1.6273324920002779

So the fastest of these options in both cases is the obvious one: 10 ** n for integers and 10.0 ** n for floats.

like image 106
kaya3 Avatar answered Mar 29 '23 14:03

kaya3


Another contender for the floats case, precompute all possible nonzero finite results and look them up:

0.0 if n < -323 else f[n] if n < 309 else inf

The preparation:

f = [10.0 ** i for i in [*range(309), *range(-323, 0)]]
inf = float('inf')

Benchmark with kaya3's exponent n = 200 as well as n = -200 as negative exponent with nonzero result and n = -5000 / n = 5000 as medium-size negative/positive exponents from your original range:

n = 200
487 ns  487 ns  488 ns  float(f'1e{n}')
108 ns  108 ns  108 ns  10.0 ** n
128 ns  129 ns  130 ns  10.0 ** n if n < 309 else inf
 72 ns   73 ns   73 ns  0.0 if n < -323 else f[n] if n < 309 else inf

n = -200
542 ns  544 ns  545 ns  float(f'1e{n}')
109 ns  109 ns  110 ns  10.0 ** n
130 ns  130 ns  131 ns  10.0 ** n if n < 309 else inf
 76 ns   76 ns   76 ns  0.0 if n < -323 else f[n] if n < 309 else inf

n = -5000
291 ns  291 ns  291 ns  float(f'1e{n}')
 99 ns   99 ns  100 ns  10.0 ** n
119 ns  120 ns  120 ns  10.0 ** n if n < 309 else inf
 34 ns   34 ns   34 ns  0.0 if n < -323 else f[n] if n < 309 else inf

n = 5000
292 ns  293 ns  293 ns  float(f'1e{n}')
 error   error   error  10.0 ** n
 33 ns   33 ns   33 ns  10.0 ** n if n < 309 else inf
 53 ns   53 ns   53 ns  0.0 if n < -323 else f[n] if n < 309 else inf

Benchmark code (Try it online!):

from timeit import repeat

solutions = [
    "float(f'1e{n}')",
    '10.0 ** n',
    '10.0 ** n if n < 309 else inf',
    '0.0 if n < -323 else f[n] if n < 309 else inf',
]

for n in 200, -200, -5000, 5000:
    print(f'{n = }')
    setup = f'''
n = {n}
f = [10.0 ** i for i in [*range(309), *range(-323, 0)]]
inf = float('inf')
'''
    for solution in solutions:
        try:
            ts = sorted(repeat(solution, setup))[:3]
        except OverflowError:
            ts = [None] * 3
        print(*('%3d ns ' % (t * 1e3) if t else ' error ' for t in ts), solution)
    print()
like image 37
Kelly Bundy Avatar answered Mar 29 '23 12:03

Kelly Bundy