Day 8 - One Python Benchmark per Day

Calculating square roots and exponents


I would be happy to hear your comments and suggestions.
Please feel free to drop me a note via twitter, email, or google+.


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.



Square root 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)


10000000 loops, best of 3: 140 ns per loop
10000000 loops, best of 3: 177 ns per loop

In [2]:
x = 988
assert(math_sqrt(x) == x**0.5)
print('both functions produce similar results')


both functions produce similar results



Power n functions

  • 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')


All functions produce similar results



Timing square root


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)))



Timing power n


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)))



Setting up plots


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')



Results


In [12]:
print_sysinfo()
plot_figures()


system   : Darwin
release  : 13.1.0
machine  : x86_64
processor: i386
interpreter: 64bit
CPU count  : 4

Python version 3.4.0
compiler GCC 4.2.1 (Apple Inc. build 5577)





Conclusion

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]:
(9.000685740426075+0.11110264582492375j)

In [3]:
import cmath
cmath.sqrt(81+2j)


Out[3]:
(9.000685740426075+0.11110264582492377j)

In [4]:
import math
math.sqrt(81+2j)


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-4-0ed1cc6d12dd> in <module>()
      1 import math
----> 2 math.sqrt(81+2j)

TypeError: can't convert complex to float