7

When discussing the question Exponentials in python x.**y vs math.pow(x, y), Alfe stated that there would be no good reason for using math.pow instead of the builtin ** operator in python.

timeit shows that math.pow is slower than ** in all cases. What is math.pow() good for anyway? Has anybody an idea where it can be of any advantage then?

We tried to convince each other with some timeit arguments an he is the winner so far ;-) -- At least the following timeit results, seem to verify that math.pow is slower than ** in all cases.

import timeit
print timeit.timeit("math.pow(2, 100)",setup='import math')
print timeit.timeit("2.0 ** 100.0")
print timeit.timeit("2 ** 100")
print timeit.timeit("2.01 ** 100.01")

Output:

0.329639911652
0.0361258983612
0.0364260673523
0.0363788604736

(ideone-shortcut)

Is there a simple explanation for the difference[1] we observe?


[1] The performances of math.pow and ** differ by one order of magnitude.

Edits:

  • literal arguments instead of variables in title
  • footnote that explicitly points on the magnitude of difference
Community
  • 1
  • 1
Wolf
  • 9,679
  • 7
  • 62
  • 108
  • 4
    This seems like a dupe of a question you linked to. – user694733 Feb 17 '15 at 14:08
  • I believe the explanation is given in one of the answers in the link you gave. One of them disassembles each case, showing how the call is rendered. – lurker Feb 17 '15 at 14:09
  • @lurker No. It's no duplicate, look at the factor 9 instead of something like 1.2. – Wolf Feb 17 '15 at 14:18
  • @user694733 I'm sure that this isn't a dup, I read the question and the answers before adding an answer myself to http://stackoverflow.com/questions/20969773/exponentials-in-python-x-y-vs-math-powx-y – Wolf Feb 17 '15 at 14:19
  • Because you have constants I suspect the power is computed at compile time. Check the disassembly. If that's the case then you need something like `print timeit.timeit("2.0 ** i", setup='i=100.0')` to make a fair comparison. – shuttle87 Feb 17 '15 at 14:21
  • @shuttle87 may have a point. Compare: `timeit.timeit('x ** y', setup='x, y = 2, 100')` with `timeit.timeit('math.pow(x, y)', setup='import math; x, y = 2, 100')`. Of course, that probably raises more questions than answers. ;) – lurker Feb 17 '15 at 14:25
  • @lurker and that's why I ask ;-) – Wolf Feb 17 '15 at 14:26
  • Yep, and that's why I up-voted your question (+1). :) – lurker Feb 17 '15 at 14:26

1 Answers1

8

Essentially the reason that the power operator looks like it's doing so well in your examples is because Python has most likely folded the constant at compile time.

import dis
dis.dis('3.0 ** 100')    
i = 100
dis.dis('3.0 ** i')

This gives the following output:

  1           0 LOAD_CONST               2 (5.153775207320113e+47)
              3 RETURN_VALUE
  1           0 LOAD_CONST               0 (3.0)
              3 LOAD_NAME                0 (i)
              6 BINARY_POWER
              7 RETURN_VALUE

You can see this run here: http://ideone.com/5Ari8o

So in this case you can see it's not actually doing a fair comparison of the performance of the power operator vs math.pow because the result has been precomputed then cached. When you are making the 3.0 ** 100 there's no computation performed, the result is just being returned. This you would expect to be much faster than any exponentiation operation performed at runtime. This is ultimately what explains your results.

For a more fair comparison you need to force the computation to occur at runtime by using a variable:

print timeit.timeit("3.0 ** i", setup='i=100')

I tried making a quick benchmark for this using the python 3.4.1 on my computer:

import timeit
trials = 1000000
print("Integer exponent:")
print("pow(2, 100)")
print(timeit.timeit(stmt="pow(2, 100)", number=trials))
print("math.pow(2, 100)")
print(timeit.timeit(stmt="m_pow(2, 100)", setup='import math; m_pow=math.pow', number=trials))
print("2 ** 100")
print(timeit.timeit(stmt="2 ** i", setup='i=100', number=trials))
print("2.0 ** 100")
print(timeit.timeit(stmt="2.0 ** i", setup='i=100', number=trials))
print("Float exponent:")
print("pow(2.0, 100.0)")
print(timeit.timeit(stmt="pow(2.0, 100.0)", number=trials))
print("math.pow(2, 100.0)")
print(timeit.timeit(stmt="m_pow(2, 100.0)", setup='import math; m_pow=math.pow', number=trials))
print("2.0 ** 100.0")
print(timeit.timeit(stmt="2.0 ** i", setup='i=100.0', number=trials))
print("2.01 ** 100.01")
print(timeit.timeit(stmt="2.01 ** i", setup='i=100.01', number=trials))

results:

Integer exponent:
pow(2, 100)
0.7596459520525322
math.pow(2, 100)
0.5203307256717318
2 ** 100
0.7334983742808263
2.0 ** 100
0.30665244505310607
Float exponent:
pow(2.0, 100.0)
0.26179656874310275
math.pow(2, 100.0)
0.34543158098034743
2.0 ** 100.0
0.1768205988074767
2.01 ** 100.01
0.18460920008178894

So it looks like the conversion to a float eats up a fair amount of the execution time.

I also added a benchmark for math.pow note that this function is not the same as the builtin pow see this for more: Difference between the built-in pow() and math.pow() for floats, in Python?

Community
  • 1
  • 1
shuttle87
  • 15,466
  • 11
  • 77
  • 106
  • A really great explanation. I finally tried `timeit.timeit("pass")` to see "the light" ;-) – Wolf Feb 17 '15 at 14:36
  • @wolf, yes that's pretty much exactly what's going on here and explains the large time discrepancy. – shuttle87 Feb 17 '15 at 14:39
  • 4
    Well, `pass` is even more efficient ;-) -- I tried something to make the comparison fair http://ideone.com/WxO0S7 – Wolf Feb 17 '15 at 14:42
  • So the answer is: It's not _so_ much faster as the first impression suggested (which was just due to the unnoticed folding). But the core of the question remains: Using `**` is always faster than using `math.pow` or using `pow`. Is there any use of `pow` and expecially `math.pow` under these circumstances (unless you need modulo computation of `pow` of course)? – Alfe Feb 17 '15 at 14:48
  • @Alfe check out http://ideone.com/WxO0S7 here you see that `math.pow` is faster for float **variables**. As expected. If you know a better answer, I'll be happy to accept *it* instead ;-) – Wolf Feb 17 '15 at 14:54
  • @Wolf, sorry, I don't see such results there. I tried my own: `timeit.timeit('mpow(a, b)', setup='from math import pow as mpow; a=2.; b=100')` and `timeit.timeit('a ** b', setup='from math import pow as mpow; a=2.; b=100')` and got `0.15636205673217773` and `0.12388300895690918` resp. So still, `**` is faster for me. – Alfe Feb 17 '15 at 15:03
  • @Wolf, your test regarding `indirect math.pow` (line 15) is buggy. You import `math.pow` as `m`, so you are not using the local function `m` you defined before (so have an indirection less and it isn't comparable to `indirect builtin pow` above). Maybe this triggered your wrong assumption that there is a case in which using `math.pow` is faster than using `**`. Actually there isn't, it still seems, and I'd still like to know if there are other scenarios in which using `math.pow` can make sense. – Alfe Feb 17 '15 at 15:10
  • @Alfe That's interesting. I think it's really the call that makes the difference (http://ideone.com/GEowHy). BTW: you're absolutely right, it's buggy, I'm trying to repair it. – Wolf Feb 17 '15 at 15:17
  • Just use `from __main__ import m` without an `as` clause. Then it (again) was much slower for me than the `**` version in `b()`. – Alfe Feb 17 '15 at 15:18
  • [fixed] Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/71090/discussion-between-wolf-and-alfe). – Wolf Feb 17 '15 at 15:20
  • @Alfe it's not always true that `pow` or `math.pow` is faster than `**`. Sometimes it's the other way around. Compare: `timeit.timeit('x ** y', setup='x, y = 2, 100')` with `timeit.timeit('math.pow(x, y)', setup='import math; x, y = 2, 100')`. I still think there are still some unanswered questions about the difference. – lurker Feb 17 '15 at 15:32
  • @lurker didn't you mix-up functions and operator? – Wolf Feb 17 '15 at 16:12
  • @Wolf maybe :) I was just indicating that there's still a significant difference in performance (comparing like cases) between `**` and `pow`. Isn't that what the whole question is about? Maybe I misunderstood... – lurker Feb 17 '15 at 16:18