For this benchmark, I only want to focus on the simplest approaches that don't require any additional libraries or modules that are not part of Python's in-built functions or the standard library.
Why? Because in most simple Python scripts I often see people using the math module for those calculations and I was curious to see if we are not better off using ,e.g., the basic arithmetic operator $x^{0.5}$ and the in-built pow
function for exponential functions.
math.sqrt(x)
x**0.5
Note that we'll import the math.sqrt() and math.pow() functions into the global namespace to reduce the overhead:
In [1]:
import math
from math import sqrt as math_sqrt
x = 81
%timeit math_sqrt(x)
%timeit math.sqrt(x)
In [2]:
x = 988
assert(math_sqrt(x) == x**0.5)
print('both functions produce similar results')
math.pow(x, n)
pow(x, n)
x**n
In [3]:
from math import pow as math_pow
x = 3.3
n = 4
assert(math_pow(x, n) == pow(x, n) == x**n)
print('All functions produce similar results')
In [5]:
import timeit
test_sqrt = [12345.54321, 54756, 23423422, 999999999999999]
timings_sqrt = {'math_module':[], 'arithmetic':[]}
for x in test_sqrt:
timings_sqrt['math_module'].append(min(timeit.Timer('math_sqrt(x)',
'from __main__ import x, math_sqrt')
.repeat(repeat=100, number=1000)))
timings_sqrt['arithmetic'].append(min(timeit.Timer('x**0.5',
'from __main__ import x')
.repeat(repeat=100, number=1000)))
In [8]:
import timeit
test_pow = list(range(1,13))
funcs_pow = ['math_module', 'inbuilt', 'arithmetic']
timings_pow = {f:[] for f in funcs_pow}
for n in test_pow:
timings_pow['math_module'].append(min(timeit.Timer('math_pow(n, n)',
'from __main__ import n, math_pow')
.repeat(repeat=100, number=1000)))
timings_pow['inbuilt'].append(min(timeit.Timer('pow(n, n)',
'from __main__ import n')
.repeat(repeat=100, number=1000)))
timings_pow['arithmetic'].append(min(timeit.Timer('n**n',
'from __main__ import n')
.repeat(repeat=100, number=1000)))
In [9]:
import platform
import multiprocessing
def print_sysinfo():
print('\nsystem :', platform.system())
print('release :', platform.release())
print('machine :', platform.machine())
print('processor:', platform.processor())
print('interpreter:', platform.architecture()[0])
print('CPU count :', multiprocessing.cpu_count())
print('\nPython version', platform.python_version())
print('compiler', platform.python_compiler())
print('\n\n')
In [10]:
%matplotlib inline
In [11]:
import matplotlib.pyplot as plt
def plot_figures():
fig = plt.figure(figsize=(8,4))
plt.plot(range(len(test_sqrt)), timings_sqrt['math_module'],
alpha=0.5, label='math.sqrt(x)', marker='o', lw=2)
plt.plot(range(len(test_sqrt)), timings_sqrt['arithmetic'],
alpha=0.5, label='x**0.5', marker='o', lw=2)
plt.legend(loc='upper left')
plt.xticks(range(len(test_sqrt)), ['$\sqrt{%s}$'%(x) for x in test_sqrt])
plt.grid()
plt.ylabel('time in milliseconds')
plt.show()
fig = plt.figure(figsize=(8,4))
plt.plot(range(len(test_pow)), timings_pow['math_module'],
alpha=0.5, label='math.pow(x, n)', marker='o', lw=2)
plt.plot(range(len(test_pow)), timings_pow['inbuilt'],
alpha=0.5, label='pow(x, n)', marker='o', lw=2)
plt.plot(range(len(test_pow)), timings_pow['arithmetic'],
alpha=0.5, label='x**n', marker='o', lw=2)
plt.legend(loc='upper left')
plt.xticks(range(len(test_pow)), ['$%s^{%s}$'%(x, x) for x in test_pow])
plt.grid()
plt.ylabel('time in milliseconds')
In [12]:
print_sysinfo()
plot_figures()
Despite the additional name-lookup for the math
-module functions, those approaches are still faster than the in-built **
operator and pow
function.
Why is the math
module more efficient?
The math
module uses the C implementations of the square root and power functions which explains the better performance.
And pow()
is really the same as the **
operator but comes with an additional function call overhead.
However, the math module comes with the disadvantage that it can't handle complex numbers. In those cases we'd need to use the cmath
module.
In [1]:
(81+2j)**0.5
Out[1]:
In [3]:
import cmath
cmath.sqrt(81+2j)
Out[3]:
In [4]:
import math
math.sqrt(81+2j)